dommy 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -1
  3. data/lib/dommy/attr.rb +56 -32
  4. data/lib/dommy/backend.rb +251 -208
  5. data/lib/dommy/blob.rb +24 -0
  6. data/lib/dommy/bridge/methods.rb +12 -2
  7. data/lib/dommy/bridge/values.rb +29 -2
  8. data/lib/dommy/bridge/wire_tags.rb +8 -4
  9. data/lib/dommy/browser.rb +328 -53
  10. data/lib/dommy/callable_invoker.rb +27 -31
  11. data/lib/dommy/character_data.rb +21 -28
  12. data/lib/dommy/class_list.rb +55 -23
  13. data/lib/dommy/cookie_jar.rb +408 -0
  14. data/lib/dommy/cookie_store.rb +104 -49
  15. data/lib/dommy/css.rb +37 -8
  16. data/lib/dommy/custom_elements.rb +360 -116
  17. data/lib/dommy/data_transfer.rb +22 -2
  18. data/lib/dommy/document.rb +1258 -346
  19. data/lib/dommy/dom_exception.rb +6 -0
  20. data/lib/dommy/dom_parser.rb +46 -14
  21. data/lib/dommy/dom_string_list.rb +36 -0
  22. data/lib/dommy/element.rb +408 -569
  23. data/lib/dommy/element_internals.rb +215 -0
  24. data/lib/dommy/encodings.rb +10 -0
  25. data/lib/dommy/event.rb +382 -148
  26. data/lib/dommy/event_source.rb +8 -34
  27. data/lib/dommy/fetch.rb +8 -12
  28. data/lib/dommy/file_reader.rb +5 -29
  29. data/lib/dommy/form_data.rb +53 -16
  30. data/lib/dommy/form_entry_list.rb +107 -138
  31. data/lib/dommy/fragment.rb +14 -16
  32. data/lib/dommy/history.rb +247 -90
  33. data/lib/dommy/html_canvas_element.rb +15 -35
  34. data/lib/dommy/html_collection.rb +76 -50
  35. data/lib/dommy/html_elements/base.rb +377 -71
  36. data/lib/dommy/html_elements/embedded.rb +350 -106
  37. data/lib/dommy/html_elements/forms/form.rb +424 -383
  38. data/lib/dommy/html_elements/forms/input.rb +351 -189
  39. data/lib/dommy/html_elements/forms/select.rb +97 -81
  40. data/lib/dommy/html_elements/interactive.rb +285 -72
  41. data/lib/dommy/html_elements/media.rb +73 -32
  42. data/lib/dommy/html_elements/metadata.rb +226 -78
  43. data/lib/dommy/html_elements/miscellaneous.rb +58 -14
  44. data/lib/dommy/html_elements/tables.rb +20 -42
  45. data/lib/dommy/html_elements.rb +28 -3
  46. data/lib/dommy/hyperlink_utils.rb +19 -8
  47. data/lib/dommy/interaction/drag_and_drop.rb +187 -0
  48. data/lib/dommy/interaction/event_synthesis.rb +182 -24
  49. data/lib/dommy/interaction/field_interactor.rb +18 -6
  50. data/lib/dommy/interaction/form_submission.rb +12 -6
  51. data/lib/dommy/interaction/key_sender.rb +102 -17
  52. data/lib/dommy/interaction.rb +1 -0
  53. data/lib/dommy/internal/accessibility_tree.rb +19 -7
  54. data/lib/dommy/internal/accessibility_visibility.rb +2 -2
  55. data/lib/dommy/internal/accessible_description.rb +3 -3
  56. data/lib/dommy/internal/accessible_name.rb +33 -45
  57. data/lib/dommy/internal/an_plus_b.rb +125 -0
  58. data/lib/dommy/internal/aria_role.rb +27 -18
  59. data/lib/dommy/internal/aria_state.rb +7 -7
  60. data/lib/dommy/internal/autofill.rb +2 -2
  61. data/lib/dommy/internal/backend_prefilter.rb +21 -18
  62. data/lib/dommy/internal/ce_reactions.rb +285 -0
  63. data/lib/dommy/internal/ce_reactions_table.rb +296 -0
  64. data/lib/dommy/internal/child_list.rb +39 -0
  65. data/lib/dommy/internal/child_navigable.rb +155 -0
  66. data/lib/dommy/internal/child_node.rb +44 -41
  67. data/lib/dommy/internal/close_watcher_manager.rb +238 -0
  68. data/lib/dommy/internal/complex_match.rb +129 -0
  69. data/lib/dommy/internal/constraint_validation.rb +130 -0
  70. data/lib/dommy/internal/css/ancestor_filter.rb +107 -0
  71. data/lib/dommy/internal/css/cascade.rb +7 -7
  72. data/lib/dommy/internal/css/cascaded_declarations.rb +14 -20
  73. data/lib/dommy/internal/css/computed_style_builder.rb +30 -3
  74. data/lib/dommy/internal/css/custom_properties.rb +63 -80
  75. data/lib/dommy/internal/css/parser.rb +53 -63
  76. data/lib/dommy/internal/css/renderability.rb +6 -0
  77. data/lib/dommy/internal/css/rule_index.rb +170 -108
  78. data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
  79. data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
  80. data/lib/dommy/internal/css/style_cache.rb +9 -6
  81. data/lib/dommy/internal/css/ua_stylesheet.rb +37 -2
  82. data/lib/dommy/internal/css_source.rb +223 -0
  83. data/lib/dommy/internal/css_syntax.rb +153 -0
  84. data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
  85. data/lib/dommy/internal/directionality.rb +2 -2
  86. data/lib/dommy/internal/document_generations.rb +91 -18
  87. data/lib/dommy/internal/document_interaction_state.rb +293 -9
  88. data/lib/dommy/internal/dom_matching.rb +7 -7
  89. data/lib/dommy/internal/element_aria.rb +65 -23
  90. data/lib/dommy/internal/element_css_inline_style.rb +26 -0
  91. data/lib/dommy/internal/element_editing.rb +41 -0
  92. data/lib/dommy/internal/element_nonce.rb +28 -0
  93. data/lib/dommy/internal/element_popover.rb +310 -0
  94. data/lib/dommy/internal/element_shadow.rb +107 -33
  95. data/lib/dommy/internal/element_state.rb +131 -26
  96. data/lib/dommy/internal/element_top_layer.rb +21 -51
  97. data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
  98. data/lib/dommy/internal/event_handler_tables.rb +241 -0
  99. data/lib/dommy/internal/event_handlers.rb +149 -0
  100. data/lib/dommy/internal/exception_report.rb +1 -1
  101. data/lib/dommy/internal/focusability.rb +267 -0
  102. data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
  103. data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +86 -0
  104. data/lib/dommy/internal/html_serialization.rb +292 -0
  105. data/lib/dommy/internal/idl_reflection.rb +202 -0
  106. data/lib/dommy/internal/idl_reflection_table.rb +589 -0
  107. data/lib/dommy/internal/infra.rb +36 -0
  108. data/lib/dommy/internal/input_type.rb +90 -20
  109. data/lib/dommy/internal/insertion_point.rb +27 -3
  110. data/lib/dommy/internal/js_number.rb +60 -0
  111. data/lib/dommy/internal/light_dismiss.rb +178 -0
  112. data/lib/dommy/internal/literal_lookup.rb +103 -0
  113. data/lib/dommy/internal/mime_type.rb +8 -0
  114. data/lib/dommy/internal/mutation_coordinator.rb +94 -60
  115. data/lib/dommy/internal/node_adopter.rb +165 -97
  116. data/lib/dommy/internal/node_factory.rb +111 -49
  117. data/lib/dommy/internal/node_identity.rb +1 -1
  118. data/lib/dommy/internal/node_traversal.rb +30 -13
  119. data/lib/dommy/internal/node_wrapper_cache.rb +34 -148
  120. data/lib/dommy/internal/observable_callback.rb +2 -5
  121. data/lib/dommy/internal/observer_options.rb +24 -14
  122. data/lib/dommy/internal/origin.rb +86 -0
  123. data/lib/dommy/internal/parent_node.rb +12 -10
  124. data/lib/dommy/internal/popover_invoker_element.rb +67 -0
  125. data/lib/dommy/internal/popover_stack.rb +120 -0
  126. data/lib/dommy/internal/post_insertion_steps.rb +146 -77
  127. data/lib/dommy/internal/reflected_attributes.rb +173 -43
  128. data/lib/dommy/internal/rendered_text/collector.rb +12 -9
  129. data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
  130. data/lib/dommy/internal/selector_arguments.rb +2 -16
  131. data/lib/dommy/internal/selector_index.rb +19 -10
  132. data/lib/dommy/internal/selector_match.rb +190 -0
  133. data/lib/dommy/internal/selector_matcher.rb +91 -256
  134. data/lib/dommy/internal/selector_parser.rb +176 -100
  135. data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
  136. data/lib/dommy/internal/shadow_root_registry.rb +14 -6
  137. data/lib/dommy/internal/sibling_positions.rb +59 -0
  138. data/lib/dommy/internal/svg_uri_reference.rb +33 -0
  139. data/lib/dommy/internal/template_content_registry.rb +11 -5
  140. data/lib/dommy/internal/text_selection.rb +185 -32
  141. data/lib/dommy/internal/toggle_events.rb +6 -4
  142. data/lib/dommy/internal/unsafe_html.rb +107 -0
  143. data/lib/dommy/internal/url_parser.rb +84 -8
  144. data/lib/dommy/internal/user_activation.rb +192 -0
  145. data/lib/dommy/internal/web_idl.rb +160 -16
  146. data/lib/dommy/internal/window_constructors.rb +34 -14
  147. data/lib/dommy/internal/xml_serialization.rb +9 -45
  148. data/lib/dommy/js/constructor_resolver.rb +18 -0
  149. data/lib/dommy/js/custom_element_bridge.rb +57 -82
  150. data/lib/dommy/js/dom_interfaces.rb +51 -15
  151. data/lib/dommy/js/host_bridge.rb +189 -40
  152. data/lib/dommy/js/host_callbacks.rb +10 -0
  153. data/lib/dommy/js/host_runtime.js +1921 -416
  154. data/lib/dommy/js/marshaller.rb +13 -4
  155. data/lib/dommy/js/module_loader.rb +46 -5
  156. data/lib/dommy/js/module_preload.rb +194 -0
  157. data/lib/dommy/js/runtime.rb +9 -0
  158. data/lib/dommy/js/script_boot.rb +200 -114
  159. data/lib/dommy/js/serialized_record.rb +32 -0
  160. data/lib/dommy/js/webidl_event_handlers.js +45 -0
  161. data/lib/dommy/js/webidl_members.js +196 -0
  162. data/lib/dommy/js/webidl_signatures.js +2055 -0
  163. data/lib/dommy/js/webidl_tables.js +72 -101
  164. data/lib/dommy/js/window_wiring.rb +9 -5
  165. data/lib/dommy/location.rb +129 -76
  166. data/lib/dommy/mathml_elements.rb +13 -0
  167. data/lib/dommy/media_query_list.rb +4 -13
  168. data/lib/dommy/message_channel.rb +137 -64
  169. data/lib/dommy/mutation_observer.rb +3 -7
  170. data/lib/dommy/navigation.rb +61 -11
  171. data/lib/dommy/navigator.rb +132 -10
  172. data/lib/dommy/node.rb +81 -44
  173. data/lib/dommy/notification.rb +10 -6
  174. data/lib/dommy/parser.rb +5 -16
  175. data/lib/dommy/promise.rb +20 -26
  176. data/lib/dommy/range.rb +20 -47
  177. data/lib/dommy/resources.rb +38 -3
  178. data/lib/dommy/scheduler.rb +176 -48
  179. data/lib/dommy/shadow_root.rb +125 -25
  180. data/lib/dommy/storage.rb +135 -25
  181. data/lib/dommy/streams.rb +2 -6
  182. data/lib/dommy/structured_data.rb +45 -0
  183. data/lib/dommy/style_declaration.rb +38 -6
  184. data/lib/dommy/svg_elements.rb +25 -11
  185. data/lib/dommy/text_codec.rb +2 -14
  186. data/lib/dommy/tree_walker.rb +12 -26
  187. data/lib/dommy/url.rb +12 -9
  188. data/lib/dommy/url_pattern.rb +1 -9
  189. data/lib/dommy/validity_state.rb +109 -107
  190. data/lib/dommy/version.rb +1 -1
  191. data/lib/dommy/web_socket.rb +10 -33
  192. data/lib/dommy/window.rb +817 -105
  193. data/lib/dommy/worker.rb +4 -28
  194. data/lib/dommy/xml_http_request.rb +32 -45
  195. data/lib/dommy.rb +43 -10
  196. metadata +52 -5
  197. data/lib/dommy/backend/makiri_adapter.rb +0 -408
  198. data/lib/dommy/internal/cookie_jar.rb +0 -29
@@ -9,14 +9,28 @@ module Dommy
9
9
  # the ones it can actually invalidate, which is the whole point — a text
10
10
  # edit inside a <p> must not throw away the rule index.
11
11
  #
12
- # Host contract: @__css_style_cache__ and #__internal_style_sheet_elements__.
12
+ # Each is the sum of the document's own count, which its mutation paths
13
+ # move, and Makiri's count of the matching edits — tree_version for the
14
+ # child lists, attribute_version for the attributes — which moves however
15
+ # the backend was edited. Every term only grows, so the sum changes with
16
+ # any of them: an edit that reached Makiri without passing through the
17
+ # document's paths still retires what hangs on the counter. Makiri counts
18
+ # no character-data edit, so a text edit that flips nothing moves nothing
19
+ # here either; one that flips `:empty` is seen through the document's
20
+ # own path alone.
21
+ #
22
+ # Host contract: @backend_doc, @__internal_css_style_cache__,
23
+ # #__internal_style_sheet_elements__ and #quirks_mode?.
13
24
  module DocumentGenerations
25
+ # The attributes SelectorIndex records an element under.
26
+ INDEXED_ATTRIBUTES = %w[id class].freeze
27
+
14
28
  def style_generation
15
- @style_generation || 0
29
+ (@style_generation || 0) + backend_tree_version
16
30
  end
17
31
 
18
32
  def dom_generation
19
- @dom_generation || 0
33
+ (@dom_generation || 0) + backend_tree_version + backend_attribute_version
20
34
  end
21
35
 
22
36
  # Moves only on childList mutations — the coarsest epoch. Keys memos
@@ -24,23 +38,56 @@ module Dommy
24
38
  # exist, in what order), like the document's <style>/<link> list: an
25
39
  # attribute-triggered cascade rebuild can then skip re-walking for them.
26
40
  def tree_generation
27
- @tree_generation || 0
41
+ (@tree_generation || 0) + backend_tree_version
42
+ end
43
+
44
+ # What the selector index (SelectorIndex) is built from: the element tree
45
+ # and each element's `id` and `class`. Nothing else it holds can go
46
+ # stale, so a value, focus or `data-*` change, which moves
47
+ # dom_generation, leaves it standing.
48
+ #
49
+ # An attribute edit the document saw and could name (see
50
+ # #__internal_note_attribute_mutation__) is accounted for: an `id` or
51
+ # `class` one moves the index's own counter, any other moves nothing.
52
+ # One Makiri counted that the document did not account for — edited
53
+ # around its mutation paths — moves the epoch, so the index is rebuilt
54
+ # rather than trusted.
55
+ def __internal_selector_index_generation__
56
+ version = backend_attribute_version
57
+ unless version == @__index_attribute_version
58
+ @__index_attribute_epoch = (@__index_attribute_epoch || 0) + 1
59
+ @__index_attribute_version = version
60
+ end
61
+ # The same frozen key while nothing moved: the matcher asks once per
62
+ # candidate, so this must not allocate.
63
+ tree = tree_generation
64
+ own = @__index_generation || 0
65
+ quirks = quirks_mode?
66
+ key = @__index_generation_key
67
+ unless key && key[0] == tree && key[1] == own && key[2] == @__index_attribute_epoch && key[3] == quirks
68
+ key = @__index_generation_key = [tree, own, @__index_attribute_epoch, quirks].freeze
69
+ end
70
+ key
28
71
  end
29
72
 
30
73
  def __internal_bump_style_generation__
31
- @style_generation = style_generation + 1
74
+ @style_generation = (@style_generation || 0) + 1
75
+ # Whatever changed may have made the focused element unfocusable:
76
+ # the next rendering update's focus fixup checks.
77
+ __internal_schedule_rendering_update__ if @active_element
32
78
  nil
33
79
  end
34
80
 
35
81
  def __internal_bump_dom_generation__
36
- @dom_generation = dom_generation + 1
82
+ @dom_generation = (@dom_generation || 0) + 1
83
+ __internal_schedule_rendering_update__ if @active_element
37
84
  nil
38
85
  end
39
86
 
40
87
  # A childList mutation: tree shape feeds both selector matching and the
41
88
  # rule -> element index, so everything is suspect.
42
89
  def __internal_note_tree_mutation__
43
- @tree_generation = tree_generation + 1
90
+ @tree_generation = (@tree_generation || 0) + 1
44
91
  __internal_bump_dom_generation__
45
92
  __internal_bump_style_generation__
46
93
  end
@@ -51,6 +98,7 @@ module Dommy
51
98
  # <style>/<link>, whose media/disabled/rel gate whole sheets.
52
99
  def __internal_note_attribute_mutation__(name, target_node)
53
100
  __internal_bump_dom_generation__
101
+ __internal_note_index_attribute__(name)
54
102
  __internal_bump_style_generation__ if __internal_style_affected_by_attribute__(name, target_node)
55
103
  end
56
104
 
@@ -62,7 +110,7 @@ module Dommy
62
110
  # Only a Text node's data participates in :empty; a comment/PI edit
63
111
  # can't change any match. `target_node` is a backend node, so its data
64
112
  # reads through the Nokogiri-compatible #content.
65
- text = target_node.respond_to?(:node_type) && target_node.node_type == 3
113
+ text = target_node.node_type == 3
66
114
  flipped = text && (old_value.to_s.empty? != target_node.content.to_s.empty?)
67
115
  __internal_bump_dom_generation__ if flipped
68
116
  if __internal_inside_style_element__(target_node) ||
@@ -95,7 +143,7 @@ module Dommy
95
143
  end
96
144
 
97
145
  def __internal_style_value_sensitive__
98
- index = @__css_style_cache__&.index
146
+ index = @__internal_css_style_cache__&.index
99
147
  index ? index.value_sensitive? : true
100
148
  end
101
149
 
@@ -109,7 +157,7 @@ module Dommy
109
157
  return @__direction_sensitive[1] if @__direction_sensitive&.first == generation
110
158
 
111
159
  sensitive = @backend_doc.css("[dir], bdi").any? do |node|
112
- Directionality.text_dependent?(node.name, node["dir"])
160
+ Directionality.text_dependent?(node.name, Backend.no_namespace_attribute_value(node, "dir"))
113
161
  end
114
162
  @__direction_sensitive = [generation, sensitive]
115
163
  sensitive
@@ -122,21 +170,27 @@ module Dommy
122
170
  def __internal_direction_sensitive_ancestor__(node)
123
171
  current = node
124
172
  while current
125
- return true if current.respond_to?(:name) && Directionality.text_dependent?(current.name, current["dir"])
173
+ if current.element? &&
174
+ Directionality.text_dependent?(current.name, Backend.no_namespace_attribute_value(current, "dir"))
175
+ return true
176
+ end
126
177
 
127
- current = current.respond_to?(:parent) ? current.parent : nil
178
+ current = current.parent
128
179
  end
129
180
  false
130
181
  end
131
182
 
132
183
  def __internal_style_affected_by_attribute__(name, target_node)
133
- owner = target_node.respond_to?(:name) ? target_node.name.to_s.downcase : nil
184
+ owner = target_node.name.to_s.downcase
134
185
  return true if owner == "style" || owner == "link"
135
186
  # The `dir` attribute drives the computed `direction` (Directionality)
136
187
  # as well as `:dir()`, neither of which is a plain attribute selector.
137
188
  return true if name.to_s.casecmp?("dir")
189
+ # The UA rules for popovers and dialogs (UAStylesheet
190
+ # .top_layer_declarations) read these with no selector of their own.
191
+ return true if name.to_s.casecmp?("popover") || name.to_s.casecmp?("open")
138
192
 
139
- index = @__css_style_cache__&.index
193
+ index = @__internal_css_style_cache__&.index
140
194
  # No RuleIndex yet: the bump is nearly free (at most it drops the
141
195
  # author_css?/counters memos), so stay conservative.
142
196
  return true unless index
@@ -145,7 +199,7 @@ module Dommy
145
199
  end
146
200
 
147
201
  def __internal_style_text_sensitive__
148
- index = @__css_style_cache__&.index
202
+ index = @__internal_css_style_cache__&.index
149
203
  index ? index.text_sensitive? : true
150
204
  end
151
205
 
@@ -156,14 +210,33 @@ module Dommy
156
210
  # between childList mutations answers this without re-walking.
157
211
  return false unless __internal_style_sheet_elements__.any? { |el| el.local_name.to_s.casecmp?("style") }
158
212
 
159
- current = node.respond_to?(:parent) ? node.parent : nil
213
+ current = node.parent
160
214
  while current
161
- return true if current.respond_to?(:name) && current.name.to_s.downcase == "style"
215
+ return true if current.name.to_s.downcase == "style"
162
216
 
163
- current = current.respond_to?(:parent) ? current.parent : nil
217
+ current = current.parent
164
218
  end
165
219
  false
166
220
  end
221
+
222
+ private
223
+
224
+ # The index-side accounting for one attribute edit, called after the
225
+ # backend took it. A non-indexed name is accounted only when it is the
226
+ # single backend edit since the last accounted one; anything else leaves
227
+ # the gap for #__internal_selector_index_generation__ to see.
228
+ def __internal_note_index_attribute__(name)
229
+ if INDEXED_ATTRIBUTES.any? { |indexed| name.casecmp?(indexed) }
230
+ @__index_generation = (@__index_generation || 0) + 1
231
+ else
232
+ version = backend_attribute_version
233
+ @__index_attribute_version = version if @__index_attribute_version == version - 1
234
+ end
235
+ end
236
+
237
+ def backend_tree_version = @backend_doc ? @backend_doc.tree_version : 0
238
+
239
+ def backend_attribute_version = @backend_doc ? @backend_doc.attribute_version : 0
167
240
  end
168
241
  end
169
242
  end
@@ -6,29 +6,230 @@ module Dommy
6
6
  # They are not tree state, but :focus and :hover match on them, so a change
7
7
  # has to tell the selector caches.
8
8
  #
9
- # Host contract: #body, and DocumentGenerations for the epoch bump a
10
- # focus or hover change owes the selector caches.
9
+ # Host contract: #body, #document_element, #default_view, and
10
+ # DocumentGenerations for the epoch bump a focus or hover change owes the
11
+ # selector caches.
11
12
  module DocumentInteractionState
12
- # Currently-focused element (or body if none). Updated via
13
- # `el.focus()` / `el.blur()`.
13
+ # DocumentOrShadowRoot's `activeElement` for the document: the focused
14
+ # element retargeted against the document — the outermost shadow host,
15
+ # for an element inside a shadow tree — or, with the viewport focused,
16
+ # the body element, else the document element.
14
17
  def active_element
15
- @active_element || body
18
+ focused = @active_element
19
+ if focused
20
+ candidate = Retargeting.retarget(focused, self)
21
+ return candidate if candidate.respond_to?(:get_root_node) && candidate.get_root_node.equal?(self)
22
+ end
23
+ body || document_element
16
24
  end
17
25
 
26
+ # Designate `el` (nil for the viewport) as the focused area, firing
27
+ # nothing — what the focus fixup does, and what the focus update steps
28
+ # do between their blur and focus events.
18
29
  def __internal_set_active_element__(el)
19
30
  # Focus is selector-observable state (:focus / :focus-within rules), so
20
31
  # a change invalidates cached query results and computed styles.
21
- __internal_note_selector_state_change__ unless @active_element.equal?(el)
32
+ return if @active_element.equal?(el)
33
+
22
34
  @active_element = el
35
+ __internal_note_selector_state_change__
23
36
  end
24
37
 
25
- # The explicitly focused element (nil when nothing holds focus) — what
26
- # :focus matches. Distinct from #active_element, which falls back to
27
- # <body> per spec.
38
+ # The explicitly focused element (nil when the viewport has the focus) —
39
+ # the focused area's DOM anchor when it is an element.
28
40
  def __internal_focused_element__
29
41
  @active_element
30
42
  end
31
43
 
44
+ # HTML's "focus update steps" from the focused area to `new_element`
45
+ # (nil, the viewport): blur and focusout at the element losing focus,
46
+ # then focus and focusin at the one gaining it, each a trusted
47
+ # FocusEvent whose relatedTarget is the other element.
48
+ #
49
+ # Whether the new focus is indicated (`:focus-visible`) is decided
50
+ # first, from how it moved (#__internal_with_focus_type__).
51
+ def __internal_focus_update__(new_element)
52
+ old = @active_element
53
+ type = @pending_focus_type || :script
54
+ explicit = @pending_focus_visible
55
+ # Focus moved by the handlers of the events below is script focus.
56
+ @pending_focus_type = nil
57
+ @pending_focus_visible = nil
58
+ return nil if old.equal?(new_element)
59
+
60
+ fire_focus_event(old, "blur", new_element) if old
61
+ fire_focus_event(old, "focusout", new_element) if old
62
+ if new_element
63
+ @focus_visible = focus_indicated?(new_element, type, explicit)
64
+ @last_focus_type = type unless type == :script
65
+ # Sequential navigation goes on from the newly focused element.
66
+ @__internal_sequential_focus_navigation_starting_point__ = new_element
67
+ else
68
+ @focus_visible = false
69
+ end
70
+ __internal_set_active_element__(new_element)
71
+ return nil unless new_element
72
+
73
+ fire_focus_event(new_element, "focus", old)
74
+ fire_focus_event(new_element, "focusin", old)
75
+ nil
76
+ end
77
+
78
+ # Run the block — focusing steps — as focus moved by `type`: :keyboard
79
+ # (sequential focus navigation), :mouse (a click) or :script, and with
80
+ # FocusOptions' `focusVisible` when the caller passed one.
81
+ def __internal_with_focus_type__(type, focus_visible: nil)
82
+ saved = [@pending_focus_type, @pending_focus_visible]
83
+ @pending_focus_type = type
84
+ @pending_focus_visible = focus_visible
85
+ yield
86
+ ensure
87
+ @pending_focus_type, @pending_focus_visible = saved
88
+ end
89
+
90
+ # Whether the user agent indicates the focus of the focused element:
91
+ # what `:focus-visible` matches on.
92
+ def __internal_focus_visible__? = @focus_visible && @active_element ? true : false
93
+
94
+ # `focus({focusVisible: true})` on the element already focused still
95
+ # indicates its focus.
96
+ def __internal_indicate_focus__
97
+ return nil if @focus_visible || @active_element.nil?
98
+
99
+ @focus_visible = true
100
+ __internal_note_selector_state_change__
101
+ nil
102
+ end
103
+
104
+ # The input modality the focus heuristics remember: a key press makes
105
+ # later script focus visible, a pointer press undoes that.
106
+ def __internal_note_keyboard_input__
107
+ @had_keyboard_event = true
108
+ nil
109
+ end
110
+
111
+ def __internal_note_pointer_input__
112
+ @had_keyboard_event = false
113
+ nil
114
+ end
115
+
116
+ # HTML's removing steps, for the focus: when the focused element, or
117
+ # a shadow-including ancestor of it, leaves the document, the viewport
118
+ # becomes the focused area. No blur fires (the "focus fixup").
119
+ def __internal_focused_subtree_removed__(element)
120
+ focused = @active_element
121
+ return nil unless focused
122
+ return nil unless Retargeting.shadow_including_inclusive_ancestor?(element, focused)
123
+
124
+ __internal_set_active_element__(nil)
125
+ nil
126
+ end
127
+
128
+ # The modal dialogs in the top layer, in the order they entered it.
129
+ # The last one blocks the document: everything outside it is inert.
130
+ def __internal_add_modal_dialog__(dialog)
131
+ (@modal_dialogs ||= []).reject! { |d| d.equal?(dialog) }
132
+ @modal_dialogs << dialog
133
+ __internal_note_selector_state_change__
134
+ nil
135
+ end
136
+
137
+ def __internal_remove_modal_dialog__(dialog)
138
+ return nil unless @modal_dialogs&.any? { |d| d.equal?(dialog) }
139
+
140
+ @modal_dialogs.reject! { |d| d.equal?(dialog) }
141
+ __internal_note_selector_state_change__
142
+ nil
143
+ end
144
+
145
+ # HTML "blocked by a modal dialog": the topmost dialog in the top layer.
146
+ def __internal_blocking_modal_dialog__ = @modal_dialogs&.last
147
+
148
+ # HTML's "open dialogs list": the open, connected dialogs, in the order
149
+ # their dialog setup steps ran (a copy).
150
+ def __internal_open_dialogs__ = (@open_dialogs ||= []).dup
151
+
152
+ def __internal_add_open_dialog__(dialog)
153
+ (@open_dialogs ||= []) << dialog unless @open_dialogs&.any? { |d| d.equal?(dialog) }
154
+ nil
155
+ end
156
+
157
+ def __internal_remove_open_dialog__(dialog)
158
+ @open_dialogs&.reject! { |d| d.equal?(dialog) }
159
+ nil
160
+ end
161
+
162
+ # HTML's "sequential focus navigation starting point": a node, or nil
163
+ # when unset (Internal::SequentialFocusNavigation).
164
+ attr_accessor :__internal_sequential_focus_navigation_starting_point__
165
+
166
+ # HTML's "popover pointerdown target" and "dialog pointerdown target",
167
+ # which light dismiss records on a press (Internal::LightDismiss).
168
+ attr_accessor :__internal_popover_pointerdown_target__, :__internal_dialog_pointerdown_target__
169
+
170
+ # HTML's steps for an element with an autofocus attribute inserted into
171
+ # a document: unless the top-level document has already processed its
172
+ # autofocus, the element joins (or moves to the end of) its autofocus
173
+ # candidates, which the next rendering update flushes.
174
+ def __internal_autofocus_inserted__(element)
175
+ return nil unless default_view.is_a?(Window)
176
+
177
+ top = __internal_top_document__
178
+ return nil if top.__internal_autofocus_processed__
179
+
180
+ top.__internal_add_autofocus_candidate__(element)
181
+ nil
182
+ end
183
+
184
+ def __internal_add_autofocus_candidate__(element)
185
+ candidates = (@autofocus_candidates ||= [])
186
+ candidates.reject! { |e| e.equal?(element) }
187
+ candidates << element
188
+ __internal_schedule_rendering_update__(:before)
189
+ nil
190
+ end
191
+
192
+ def __internal_autofocus_processed__ = @autofocus_processed ? true : false
193
+
194
+ # What the dialog and popover focusing steps do once they have focused
195
+ # something: the page's autofocus is settled.
196
+ def __internal_autofocus_done__
197
+ top = __internal_top_document__
198
+ top.instance_variable_set(:@autofocus_candidates, [])
199
+ top.instance_variable_set(:@autofocus_processed, true)
200
+ nil
201
+ end
202
+
203
+ # The active document of this document's top-level traversable: up
204
+ # through the frames that host it.
205
+ def __internal_top_document__
206
+ document = self
207
+ seen = 0
208
+ while (view = document.default_view).respond_to?(:frame_element) && (frame = view.frame_element) &&
209
+ (seen += 1) < 64
210
+ document = frame.owner_document
211
+ end
212
+ document
213
+ end
214
+
215
+ # Ask for the next rendering update (Scheduler#request_rendering_update),
216
+ # once per phase: before the frame's rAF callbacks it flushes the
217
+ # autofocus candidates, after them it runs the focus fixup.
218
+ def __internal_schedule_rendering_update__(phase = :after)
219
+ pending = (@rendering_update_pending ||= {})
220
+ return nil if pending[phase]
221
+
222
+ view = default_view
223
+ return nil unless view.is_a?(Window) && view.document.equal?(self)
224
+
225
+ pending[phase] = true
226
+ view.scheduler.request_rendering_update(lambda do
227
+ pending.delete(phase)
228
+ phase == :before ? flush_autofocus_candidates : focus_fixup
229
+ end, phase: phase)
230
+ nil
231
+ end
232
+
32
233
  # The element the (virtual) pointer hovers — :hover matches it and its
33
234
  # ancestors. Set from tests or capybara-dommy's Node#hover; nil clears.
34
235
  def __internal_hovered_element__
@@ -42,6 +243,89 @@ module Dommy
42
243
  __internal_note_selector_state_change__
43
244
  nil
44
245
  end
246
+
247
+ private
248
+
249
+ # Input types whose controls take keyboard input (they would bring up
250
+ # a virtual keyboard).
251
+ KEYBOARD_INPUT_TYPES = %w[text search url tel email password number date month week time
252
+ datetime-local].freeze
253
+
254
+ # Selectors' suggested heuristics for when to indicate focus: an
255
+ # explicit `focusVisible` wins; keyboard focus is indicated; a click
256
+ # indicates it only on an element that takes keyboard input; script
257
+ # focus is indicated unless the last focus came from a pointer with no
258
+ # key pressed since — or the element takes keyboard input.
259
+ def focus_indicated?(element, type, explicit)
260
+ return explicit unless explicit.nil?
261
+
262
+ case type
263
+ when :keyboard then true
264
+ when :mouse then keyboard_input?(element)
265
+ else @last_focus_type != :mouse || @had_keyboard_event || keyboard_input?(element)
266
+ end
267
+ end
268
+
269
+ def keyboard_input?(element)
270
+ return true if Focusability.editing_host?(element)
271
+ return false unless element.namespace_uri == Namespaces::HTML
272
+
273
+ case element.local_name
274
+ when "textarea" then true
275
+ when "input" then KEYBOARD_INPUT_TYPES.include?(element.type.to_s)
276
+ else false
277
+ end
278
+ end
279
+
280
+ # HTML's focus fixup (in "update the rendering"): a focused element
281
+ # that stopped being a focusable area (disabled, hidden, made inert)
282
+ # gives the focus to the viewport, firing blur.
283
+ def focus_fixup
284
+ focused = @active_element
285
+ __internal_focus_update__(nil) if focused && !Focusability.focusable_area?(focused)
286
+ end
287
+
288
+ # HTML "flush autofocus candidates", for a top-level document: the
289
+ # first candidate that is (or delegates to) a focusable area takes the
290
+ # focus — unless something is focused already or the document has a
291
+ # target element, which settles the matter without focusing anything.
292
+ def flush_autofocus_candidates
293
+ return if @autofocus_processed
294
+
295
+ candidates = @autofocus_candidates
296
+ return if candidates.nil? || candidates.empty?
297
+
298
+ if @active_element || Internal.target_id(self)
299
+ candidates.clear
300
+ @autofocus_processed = true
301
+ return
302
+ end
303
+
304
+ until candidates.empty?
305
+ element = candidates.shift
306
+ doc = element.owner_document
307
+ next unless element.is_connected? && doc.__internal_top_document__.equal?(self)
308
+ next if Internal.target_id(doc)
309
+
310
+ target = Focusability.focusable_area?(element) ? element : Focusability.focusable_area_for(element)
311
+ next if target.nil?
312
+
313
+ candidates.clear
314
+ @autofocus_processed = true
315
+ target == :viewport ? __internal_focus_update__(nil) : Focusability.run_focusing_steps(target)
316
+ end
317
+ end
318
+
319
+ # HTML "fire a focus event": a FocusEvent with the related target, the
320
+ # window as its view, and the composed flag; focusin and focusout
321
+ # (UI Events) bubble.
322
+ def fire_focus_event(target, type, related)
323
+ bubbles = type == "focusin" || type == "focusout"
324
+ view = default_view
325
+ event = FocusEvent.new(type, "bubbles" => bubbles, "composed" => true, "relatedTarget" => related,
326
+ "view" => view.is_a?(Window) ? view : nil)
327
+ target.dispatch_event(event.__internal_mark_trusted__)
328
+ end
45
329
  end
46
330
  end
47
331
  end
@@ -91,9 +91,9 @@ module Dommy
91
91
  # detected through the computed style as well. No layout: geometry-
92
92
  # dependent invisibility stays out of scope.
93
93
  def visible?(element)
94
- return true unless element.respond_to?(:__dommy_backend_node__)
94
+ node = element.__dommy_backend_node__ if element.is_a?(Node)
95
+ return true unless node
95
96
 
96
- node = element.__dommy_backend_node__
97
97
  return false if node_invisible_self?(node)
98
98
 
99
99
  NodeTraversal.each_ancestor(node) do |ancestor|
@@ -170,12 +170,12 @@ module Dommy
170
170
  # @api private (kept module-level only because visible? calls them)
171
171
 
172
172
  def node_invisible_self?(node)
173
- return false unless node.respond_to?(:[])
173
+ return false unless node.element?
174
174
 
175
- return true if node["hidden"]
176
- return true if node.respond_to?(:name) && node.name == "input" && node["type"] == "hidden"
175
+ return true if Backend.no_namespace_attribute_value(node, "hidden")
176
+ return true if node.name == "input" && Backend.no_namespace_attribute_value(node, "type") == "hidden"
177
177
 
178
- style = node["style"].to_s
178
+ style = Backend.no_namespace_attribute_value(node, "style").to_s
179
179
  style.match?(/display\s*:\s*none/i) ||
180
180
  style.match?(/visibility\s*:\s*hidden/i) ||
181
181
  # Inline zero opacity hides too — the same number grammar as the
@@ -189,7 +189,7 @@ module Dommy
189
189
  NON_RENDERED_TAGS = %w[head script style template noscript].freeze
190
190
 
191
191
  def non_rendering_tag?(node)
192
- node.respond_to?(:name) && NON_RENDERED_TAGS.include?(node.name)
192
+ NON_RENDERED_TAGS.include?(node.name)
193
193
  end
194
194
 
195
195
  # Visible text of a node's subtree: like `text_content`, but excluding