dommy 0.16.0 → 0.17.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.
- checksums.yaml +4 -4
- data/lib/dommy/animation.rb +1 -0
- data/lib/dommy/backend.rb +3 -0
- data/lib/dommy/blob.rb +6 -0
- data/lib/dommy/browser.rb +12 -4
- data/lib/dommy/character_data.rb +1 -13
- data/lib/dommy/cookie_store.rb +2 -1
- data/lib/dommy/document.rb +67 -30
- data/lib/dommy/element.rb +11 -0
- data/lib/dommy/element_internals.rb +12 -0
- data/lib/dommy/encodings/legacy_multibyte.rb +800 -0
- data/lib/dommy/encodings/multibyte_overrides.rb +76 -0
- data/lib/dommy/encodings/prescan.rb +234 -0
- data/lib/dommy/encodings.rb +121 -57
- data/lib/dommy/event.rb +107 -54
- data/lib/dommy/event_source.rb +1 -0
- data/lib/dommy/fetch.rb +244 -53
- data/lib/dommy/file_reader.rb +1 -0
- data/lib/dommy/form_data.rb +22 -9
- data/lib/dommy/form_entry_list.rb +3 -3
- data/lib/dommy/html_canvas_element.rb +85 -12
- data/lib/dommy/html_elements/base.rb +1 -0
- data/lib/dommy/html_elements/forms/input.rb +3 -0
- data/lib/dommy/html_elements/interactive.rb +56 -48
- data/lib/dommy/html_elements/metadata.rb +23 -0
- data/lib/dommy/interaction/form_submission.rb +24 -42
- data/lib/dommy/internal/accessibility_tree.rb +149 -162
- data/lib/dommy/internal/accessibility_visibility.rb +16 -17
- data/lib/dommy/internal/accessible_name.rb +231 -246
- data/lib/dommy/internal/aria_role.rb +182 -195
- data/lib/dommy/internal/aria_snapshot.rb +35 -38
- data/lib/dommy/internal/aria_state.rb +102 -118
- data/lib/dommy/internal/autofill.rb +75 -75
- data/lib/dommy/internal/backend_prefilter.rb +127 -130
- data/lib/dommy/internal/child_navigable.rb +86 -83
- data/lib/dommy/internal/close_watcher_manager.rb +1 -0
- data/lib/dommy/internal/css/ancestor_filter.rb +64 -64
- data/lib/dommy/internal/css/calc.rb +90 -71
- data/lib/dommy/internal/css/cascade.rb +69 -67
- data/lib/dommy/internal/css/color.rb +249 -187
- data/lib/dommy/internal/css/computed_style_builder.rb +34 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +50 -11
- data/lib/dommy/internal/css/counters.rb +67 -67
- data/lib/dommy/internal/css/custom_properties.rb +114 -114
- data/lib/dommy/internal/css/parser.rb +277 -239
- data/lib/dommy/internal/css/property_registry.rb +321 -302
- data/lib/dommy/internal/css/renderability.rb +37 -35
- data/lib/dommy/internal/css/rule_index.rb +2 -2
- data/lib/dommy/internal/css/rule_index_inputs.rb +2 -2
- data/lib/dommy/internal/css/supports.rb +115 -113
- data/lib/dommy/internal/css/ua_stylesheet.rb +55 -53
- data/lib/dommy/internal/css_rule_text.rb +121 -119
- data/lib/dommy/internal/css_source.rb +1 -3
- data/lib/dommy/internal/css_syntax.rb +157 -92
- data/lib/dommy/internal/declarative_shadow_roots.rb +1 -0
- data/lib/dommy/internal/directionality.rb +120 -123
- data/lib/dommy/internal/document_live_ranges.rb +26 -1
- data/lib/dommy/internal/dom_matching.rb +185 -184
- data/lib/dommy/internal/element_geometry.rb +8 -1
- data/lib/dommy/internal/element_matching.rb +71 -69
- data/lib/dommy/internal/element_shadow.rb +1 -13
- data/lib/dommy/internal/element_state.rb +255 -265
- data/lib/dommy/internal/event_handlers.rb +66 -64
- data/lib/dommy/internal/exception_report.rb +88 -86
- data/lib/dommy/internal/focusability.rb +205 -205
- data/lib/dommy/internal/global_functions.rb +49 -47
- data/lib/dommy/internal/idl_reflection.rb +99 -97
- data/lib/dommy/internal/js_number.rb +35 -33
- data/lib/dommy/internal/light_dismiss.rb +130 -132
- data/lib/dommy/internal/literal_lookup.rb +75 -75
- data/lib/dommy/internal/mime_type.rb +54 -49
- data/lib/dommy/internal/mutation_coordinator.rb +46 -0
- data/lib/dommy/internal/mutation_observer_microtask.rb +60 -0
- data/lib/dommy/internal/namespaces.rb +51 -49
- data/lib/dommy/internal/node_adopter.rb +25 -1
- data/lib/dommy/internal/node_equality.rb +61 -59
- data/lib/dommy/internal/post_insertion_steps.rb +1 -0
- data/lib/dommy/internal/referrer.rb +144 -0
- data/lib/dommy/internal/rendered_text/collector.rb +22 -3
- data/lib/dommy/internal/rendered_text/computed_box.rb +3 -2
- data/lib/dommy/internal/rendered_text/fragment.rb +37 -38
- data/lib/dommy/internal/rendered_text/items.rb +7 -5
- data/lib/dommy/internal/selector_matcher.rb +361 -359
- data/lib/dommy/internal/selector_parser.rb +74 -62
- data/lib/dommy/internal/sequential_focus_navigation.rb +150 -151
- data/lib/dommy/internal/shadow_root_registry.rb +7 -0
- data/lib/dommy/internal/slot_assignment.rb +117 -0
- data/lib/dommy/internal/slot_signals.rb +125 -0
- data/lib/dommy/internal/unsafe_html.rb +79 -77
- data/lib/dommy/internal/url_parser.rb +5 -29
- data/lib/dommy/internal/web_idl.rb +158 -153
- data/lib/dommy/internal/window_bound.rb +14 -0
- data/lib/dommy/internal/window_constructors.rb +3 -0
- data/lib/dommy/internal/xml_serialization.rb +298 -296
- data/lib/dommy/js/dom_interfaces.rb +13 -1
- data/lib/dommy/js/host_bridge.rb +23 -1
- data/lib/dommy/js/host_callbacks.rb +7 -0
- data/lib/dommy/js/host_runtime.js +625 -61
- data/lib/dommy/js/import_map.rb +1 -1
- data/lib/dommy/js/observable_runtime.js +295 -128
- data/lib/dommy/js/webidl_members.js +1 -1
- data/lib/dommy/js/webidl_signatures.js +35 -6
- data/lib/dommy/media_query_list.rb +1 -0
- data/lib/dommy/message_channel.rb +2 -0
- data/lib/dommy/mutation_observer.rb +25 -29
- data/lib/dommy/navigator.rb +4 -1
- data/lib/dommy/node.rb +4 -0
- data/lib/dommy/notification.rb +1 -0
- data/lib/dommy/promise.rb +12 -0
- data/lib/dommy/range.rb +20 -3
- data/lib/dommy/shadow_root.rb +25 -1
- data/lib/dommy/streams.rb +18 -0
- data/lib/dommy/style_declaration.rb +15 -4
- data/lib/dommy/text_codec.rb +7 -5
- data/lib/dommy/tree_walker.rb +68 -61
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +1 -0
- data/lib/dommy/window.rb +22 -4
- data/lib/dommy/worker.rb +1 -0
- data/lib/dommy/xml_http_request.rb +12 -2
- data/lib/dommy.rb +17 -2
- metadata +11 -3
|
@@ -56,291 +56,276 @@ module Dommy
|
|
|
56
56
|
|
|
57
57
|
extend TextFlattening
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
# Input types for which the placeholder contributes the accessible name
|
|
60
|
+
# (the text-like inputs); type=number / range / date / … do not.
|
|
61
|
+
PLACEHOLDER_TYPES = %w[text search tel url email password].freeze
|
|
60
62
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
def compute(element)
|
|
65
|
-
squish(name_of(element, Traversal.start, referenced: false, allow_content: false))
|
|
66
|
-
end
|
|
63
|
+
# Elements that generate a block-level box by the UA stylesheet — used as
|
|
64
|
+
# the fallback when an element has no computed `display`.
|
|
65
|
+
BLOCK_TAGS = CSS::UAStylesheet::BLOCK_LEVEL_TAGS
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return "" unless node.is_a?(Node) && node.__dommy_backend_node__
|
|
75
|
-
|
|
76
|
-
# 1. aria-labelledby (not when already inside a labelledby traversal).
|
|
77
|
-
unless referenced
|
|
78
|
-
labelled = labelledby_name(node, traversal)
|
|
79
|
-
return labelled unless labelled.nil?
|
|
67
|
+
class << self
|
|
68
|
+
# The accessible name string ("" when none). ASCII whitespace runs are
|
|
69
|
+
# collapsed and the result trimmed, matching how browsers / Playwright
|
|
70
|
+
# flatten an accessible name.
|
|
71
|
+
def compute(element)
|
|
72
|
+
squish(name_of(element, Traversal.start, referenced: false, allow_content: false))
|
|
80
73
|
end
|
|
81
74
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# separator between sibling text runs ("button" + " " + "label").
|
|
99
|
-
return content unless content.empty?
|
|
75
|
+
# Join the accessible names of the elements an IDREF-list attribute
|
|
76
|
+
# (aria-labelledby / aria-describedby) associates — the ones its
|
|
77
|
+
# reflection gives, explicitly set or found by ID in the element's own
|
|
78
|
+
# tree. Returns nil when there are none, so the caller falls through.
|
|
79
|
+
# Each referenced node is named with `referenced: true` (it does not
|
|
80
|
+
# restart a labelledby traversal) and `allow_content: true`.
|
|
81
|
+
def referenced_names(node, attribute, traversal = Traversal.start)
|
|
82
|
+
refs = node.respond_to?(:__internal_aria_associated_elements__) ? node.__internal_aria_associated_elements__(attribute) : nil
|
|
83
|
+
return nil if refs.nil? || refs.empty?
|
|
84
|
+
|
|
85
|
+
parts = refs.map do |ref|
|
|
86
|
+
name_of(ref, traversal.naming(hidden_root: AccessibilityVisibility.hidden_for_name?(ref)),
|
|
87
|
+
referenced: true, allow_content: true)
|
|
88
|
+
end
|
|
89
|
+
joined = parts.join(" ").strip
|
|
90
|
+
joined.empty? ? nil : joined
|
|
100
91
|
end
|
|
101
92
|
|
|
102
|
-
#
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
# Whether an element generates a block-level box, so its text is separated
|
|
94
|
+
# from siblings by whitespace in name-from-content (inline boxes glue). The
|
|
95
|
+
# computed `display` decides (honoring author CSS); without one the
|
|
96
|
+
# UA-default block-tag set is used so table cells / list items still
|
|
97
|
+
# separate.
|
|
98
|
+
def block_level?(element)
|
|
99
|
+
display = computed_display(element)
|
|
100
|
+
return BLOCK_TAGS.include?(element.local_name.to_s.downcase) if display.nil?
|
|
105
101
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return placeholder if placeholder
|
|
102
|
+
!display.start_with?("inline") && !%w[none contents].include?(display)
|
|
103
|
+
end
|
|
109
104
|
|
|
110
|
-
|
|
111
|
-
end
|
|
105
|
+
private
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
# `referenced`: this node was reached through aria-labelledby, so it must
|
|
108
|
+
# not start a fresh labelledby traversal (prevents loops, and lets a
|
|
109
|
+
# self-reference fall through to its own aria-label/content).
|
|
110
|
+
# `allow_content`: name-from-content is permitted regardless of role (true
|
|
111
|
+
# for referenced and recursed-into nodes); at the top it is role-gated.
|
|
112
|
+
def name_of(node, traversal, referenced:, allow_content:)
|
|
113
|
+
return "" unless node.is_a?(Node) && node.__dommy_backend_node__
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# restart a labelledby traversal) and `allow_content: true`.
|
|
125
|
-
def referenced_names(node, attribute, traversal = Traversal.start)
|
|
126
|
-
refs = node.respond_to?(:__internal_aria_associated_elements__) ? node.__internal_aria_associated_elements__(attribute) : nil
|
|
127
|
-
return nil if refs.nil? || refs.empty?
|
|
128
|
-
|
|
129
|
-
parts = refs.map do |ref|
|
|
130
|
-
name_of(ref, traversal.naming(hidden_root: AccessibilityVisibility.hidden_for_name?(ref)),
|
|
131
|
-
referenced: true, allow_content: true)
|
|
132
|
-
end
|
|
133
|
-
joined = parts.join(" ").strip
|
|
134
|
-
joined.empty? ? nil : joined
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# <label>/alt host-language names. Returns nil when not applicable.
|
|
138
|
-
def native_name(node, traversal)
|
|
139
|
-
tag = node.tag_name.to_s.downcase
|
|
140
|
-
case tag
|
|
141
|
-
when "img", "area"
|
|
142
|
-
alt = node.__internal_attribute_value__("alt")
|
|
143
|
-
alt.nil? ? nil : alt
|
|
144
|
-
when "input"
|
|
145
|
-
input_native_name(node, traversal)
|
|
146
|
-
when "fieldset"
|
|
147
|
-
child_element_name(node, "legend", traversal)
|
|
148
|
-
when "figure"
|
|
149
|
-
child_element_name(node, "figcaption", traversal)
|
|
150
|
-
when "table"
|
|
151
|
-
child_element_name(node, "caption", traversal)
|
|
152
|
-
when *LABELABLE
|
|
153
|
-
label_text(node, traversal)
|
|
154
|
-
end
|
|
155
|
-
end
|
|
115
|
+
# 1. aria-labelledby (not when already inside a labelledby traversal).
|
|
116
|
+
unless referenced
|
|
117
|
+
labelled = labelledby_name(node, traversal)
|
|
118
|
+
return labelled unless labelled.nil?
|
|
119
|
+
end
|
|
156
120
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
121
|
+
# 2. aria-label.
|
|
122
|
+
aria_label = node.__internal_aria_value__("aria-label").to_s
|
|
123
|
+
return aria_label unless aria_label.strip.empty?
|
|
124
|
+
|
|
125
|
+
# 3. Native host-language labeling.
|
|
126
|
+
native = native_name(node, traversal)
|
|
127
|
+
return native unless native.nil?
|
|
128
|
+
|
|
129
|
+
# 4. Name from content (role-permitting, or when recursing). Guard
|
|
130
|
+
# against content cycles with the visited set.
|
|
131
|
+
key = node.__dommy_backend_node__
|
|
132
|
+
names_from_content = allow_content || NAME_FROM_CONTENT.include?(node.computed_role) ||
|
|
133
|
+
NAME_FROM_CONTENT_TAGS.include?(node.local_name.to_s.downcase)
|
|
134
|
+
if names_from_content && !traversal.seen?(key)
|
|
135
|
+
content = content_name(node, traversal.entering(key))
|
|
136
|
+
# Preserve whitespace-only content: a space deep in the subtree is the
|
|
137
|
+
# separator between sibling text runs ("button" + " " + "label").
|
|
138
|
+
return content unless content.empty?
|
|
139
|
+
end
|
|
162
140
|
|
|
163
|
-
|
|
164
|
-
|
|
141
|
+
# 5. Tooltip (title) fallback.
|
|
142
|
+
title = node.__internal_attribute_value__("title").to_s
|
|
143
|
+
return title unless title.empty?
|
|
165
144
|
|
|
166
|
-
|
|
167
|
-
|
|
145
|
+
# 6. Placeholder — the lowest-priority name source (below the title).
|
|
146
|
+
placeholder = placeholder_fallback(node)
|
|
147
|
+
return placeholder if placeholder
|
|
168
148
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
PLACEHOLDER_TYPES = %w[text search tel url email password].freeze
|
|
149
|
+
""
|
|
150
|
+
end
|
|
172
151
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
152
|
+
# aria-labelledby: join each referenced element's name. Returns nil when
|
|
153
|
+
# the attribute is absent/empty (so the caller falls through).
|
|
154
|
+
def labelledby_name(node, traversal)
|
|
155
|
+
referenced_names(node, "aria-labelledby", traversal)
|
|
156
|
+
end
|
|
177
157
|
|
|
178
|
-
|
|
179
|
-
|
|
158
|
+
# <label>/alt host-language names. Returns nil when not applicable.
|
|
159
|
+
def native_name(node, traversal)
|
|
160
|
+
tag = node.tag_name.to_s.downcase
|
|
161
|
+
case tag
|
|
162
|
+
when "img", "area"
|
|
163
|
+
alt = node.__internal_attribute_value__("alt")
|
|
164
|
+
alt.nil? ? nil : alt
|
|
165
|
+
when "input"
|
|
166
|
+
input_native_name(node, traversal)
|
|
167
|
+
when "fieldset"
|
|
168
|
+
child_element_name(node, "legend", traversal)
|
|
169
|
+
when "figure"
|
|
170
|
+
child_element_name(node, "figcaption", traversal)
|
|
171
|
+
when "table"
|
|
172
|
+
child_element_name(node, "caption", traversal)
|
|
173
|
+
when *LABELABLE
|
|
174
|
+
label_text(node, traversal)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
180
177
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return nil unless tag == "input"
|
|
178
|
+
# The name of the first child element with the given tag (e.g. a
|
|
179
|
+
# <fieldset>'s <legend>, a <table>'s <caption>). nil when absent.
|
|
180
|
+
def child_element_name(node, tag, traversal)
|
|
181
|
+
bn = node.__dommy_backend_node__
|
|
182
|
+
return nil unless bn
|
|
187
183
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
184
|
+
child = bn.children.find { |c| c.name.to_s.casecmp?(tag) }
|
|
185
|
+
return nil unless child
|
|
191
186
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
placeholder.empty? ? nil : placeholder
|
|
195
|
-
end
|
|
187
|
+
name_of(node.document.wrap_node(child), traversal, referenced: false, allow_content: true)
|
|
188
|
+
end
|
|
196
189
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return nil if labels.empty?
|
|
202
|
-
|
|
203
|
-
text = labels.map do |label|
|
|
204
|
-
name_of(label, traversal.naming(skip: node, hidden_root: AccessibilityVisibility.hidden_for_name?(label)),
|
|
205
|
-
referenced: false, allow_content: true)
|
|
206
|
-
end.join(" ").strip
|
|
207
|
-
text.empty? ? nil : text
|
|
208
|
-
end
|
|
190
|
+
def input_native_name(node, traversal)
|
|
191
|
+
type = node.__internal_attribute_value__("type").to_s.downcase
|
|
192
|
+
return node.__internal_attribute_value__("alt") || node.__internal_attribute_value__("value").to_s if type == "image"
|
|
193
|
+
return node.__internal_attribute_value__("value").to_s if %w[button submit reset].include?(type)
|
|
209
194
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# document order). Anything else can still sit inside a label, so the
|
|
213
|
-
# ancestor walk remains as the fallback.
|
|
214
|
-
def associated_labels(node)
|
|
215
|
-
return node.labels.to_a if node.respond_to?(:labels) && node.labels
|
|
195
|
+
label_text(node, traversal)
|
|
196
|
+
end
|
|
216
197
|
|
|
217
|
-
|
|
218
|
-
|
|
198
|
+
# The placeholder names a text-like input / textarea, but only as the
|
|
199
|
+
# lowest-priority source (below the title).
|
|
200
|
+
def placeholder_fallback(node)
|
|
201
|
+
tag = node.tag_name.to_s.downcase
|
|
202
|
+
return placeholder_name(node) if tag == "textarea"
|
|
203
|
+
return nil unless tag == "input"
|
|
219
204
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return node.document.wrap_node(bn) if bn.name.to_s.casecmp?("label")
|
|
205
|
+
type = node.__internal_attribute_value__("type").to_s.downcase
|
|
206
|
+
(type.empty? || PLACEHOLDER_TYPES.include?(type)) ? placeholder_name(node) : nil
|
|
207
|
+
end
|
|
224
208
|
|
|
225
|
-
|
|
209
|
+
def placeholder_name(node)
|
|
210
|
+
placeholder = node.__internal_attribute_value__("placeholder").to_s
|
|
211
|
+
placeholder.empty? ? nil : placeholder
|
|
226
212
|
end
|
|
227
|
-
nil
|
|
228
|
-
end
|
|
229
213
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
next "" unless wrapped
|
|
243
|
-
# The control a label names contributes nothing to that label's text
|
|
244
|
-
# — `<label>Name <select>…</select></label>` names the select "Name",
|
|
245
|
-
# not "Name" plus its own options.
|
|
246
|
-
next "" if traversal.skip && NodeIdentity.same_node?(traversal.skip, child)
|
|
247
|
-
# A hidden subtree is not part of the name computed from content —
|
|
248
|
-
# unless the traversal started at a hidden node, which brings its
|
|
249
|
-
# whole subtree along.
|
|
250
|
-
next "" if !traversal.hidden_root && AccessibilityVisibility.hidden_for_name?(wrapped)
|
|
251
|
-
|
|
252
|
-
name = name_of(wrapped, traversal, referenced: false, allow_content: true)
|
|
253
|
-
# Concatenate contributions directly (inline content glues:
|
|
254
|
-
# "button" + "" + "label" -> "buttonlabel"); a block-level box is
|
|
255
|
-
# padded with spaces so sibling cells / blocks separate
|
|
256
|
-
# ("Profile" + "A" -> "Profile A"). compute collapses the runs.
|
|
257
|
-
block_level?(wrapped) ? " #{name} " : name
|
|
258
|
-
else
|
|
259
|
-
""
|
|
260
|
-
end
|
|
261
|
-
end.join
|
|
214
|
+
# The concatenated text of the <label>s associated with a control:
|
|
215
|
+
# label[for=id] anywhere, plus an ancestor <label>. nil → no labels.
|
|
216
|
+
def label_text(node, traversal)
|
|
217
|
+
labels = associated_labels(node)
|
|
218
|
+
return nil if labels.empty?
|
|
219
|
+
|
|
220
|
+
text = labels.map do |label|
|
|
221
|
+
name_of(label, traversal.naming(skip: node, hidden_root: AccessibilityVisibility.hidden_for_name?(label)),
|
|
222
|
+
referenced: false, allow_content: true)
|
|
223
|
+
end.join(" ").strip
|
|
224
|
+
text.empty? ? nil : text
|
|
225
|
+
end
|
|
262
226
|
|
|
263
|
-
|
|
264
|
-
|
|
227
|
+
# A labelable control answers its own `labels` (HTML's live list, which
|
|
228
|
+
# already covers both the `for=` references and a wrapping label, in
|
|
229
|
+
# document order). Anything else can still sit inside a label, so the
|
|
230
|
+
# ancestor walk remains as the fallback.
|
|
231
|
+
def associated_labels(node)
|
|
232
|
+
return node.labels.to_a if node.respond_to?(:labels) && node.labels
|
|
265
233
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
BLOCK_TAGS = CSS::UAStylesheet::BLOCK_LEVEL_TAGS
|
|
234
|
+
[closest_label(node)].compact
|
|
235
|
+
end
|
|
269
236
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
# separate.
|
|
275
|
-
def block_level?(element)
|
|
276
|
-
display = computed_display(element)
|
|
277
|
-
return BLOCK_TAGS.include?(element.local_name.to_s.downcase) if display.nil?
|
|
237
|
+
def closest_label(node)
|
|
238
|
+
bn = node.__dommy_backend_node__&.parent
|
|
239
|
+
while bn
|
|
240
|
+
return node.document.wrap_node(bn) if bn.name.to_s.casecmp?("label")
|
|
278
241
|
|
|
279
|
-
|
|
280
|
-
|
|
242
|
+
bn = bn.parent
|
|
243
|
+
end
|
|
244
|
+
nil
|
|
245
|
+
end
|
|
281
246
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
247
|
+
# Concatenate child text nodes and the names of element children, with the
|
|
248
|
+
# `::before` content prepended and `::after` content appended (accname
|
|
249
|
+
# name-from-content folds in generated content).
|
|
250
|
+
def content_name(node, traversal)
|
|
251
|
+
bn = node.__dommy_backend_node__
|
|
252
|
+
return "" unless bn
|
|
253
|
+
|
|
254
|
+
children = bn.children.map do |child|
|
|
255
|
+
if child.text?
|
|
256
|
+
child.text.to_s
|
|
257
|
+
elsif child.element?
|
|
258
|
+
wrapped = node.document.wrap_node(child)
|
|
259
|
+
next "" unless wrapped
|
|
260
|
+
# The control a label names contributes nothing to that label's text
|
|
261
|
+
# — `<label>Name <select>…</select></label>` names the select "Name",
|
|
262
|
+
# not "Name" plus its own options.
|
|
263
|
+
next "" if traversal.skip && NodeIdentity.same_node?(traversal.skip, child)
|
|
264
|
+
# A hidden subtree is not part of the name computed from content —
|
|
265
|
+
# unless the traversal started at a hidden node, which brings its
|
|
266
|
+
# whole subtree along.
|
|
267
|
+
next "" if !traversal.hidden_root && AccessibilityVisibility.hidden_for_name?(wrapped)
|
|
268
|
+
|
|
269
|
+
name = name_of(wrapped, traversal, referenced: false, allow_content: true)
|
|
270
|
+
# Concatenate contributions directly (inline content glues:
|
|
271
|
+
# "button" + "" + "label" -> "buttonlabel"); a block-level box is
|
|
272
|
+
# padded with spaces so sibling cells / blocks separate
|
|
273
|
+
# ("Profile" + "A" -> "Profile A"). compute collapses the runs.
|
|
274
|
+
block_level?(wrapped) ? " #{name} " : name
|
|
275
|
+
else
|
|
276
|
+
""
|
|
277
|
+
end
|
|
278
|
+
end.join
|
|
279
|
+
|
|
280
|
+
pseudo_content(node, "::before") + children + pseudo_content(node, "::after")
|
|
281
|
+
end
|
|
288
282
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
content_text(decl.get_property_value("content"), node)
|
|
296
|
-
end
|
|
283
|
+
# nil when the element has no computed `display` (it is in no
|
|
284
|
+
# document), so block_level? falls back to the UA-default tag set.
|
|
285
|
+
def computed_display(element)
|
|
286
|
+
value = Internal::CSS::Cascade.computed_style(element)["display"].to_s
|
|
287
|
+
value.empty? ? nil : value
|
|
288
|
+
end
|
|
297
289
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
v = value.to_s.strip
|
|
306
|
-
return "" if v.empty? || v == "none" || v == "normal"
|
|
307
|
-
|
|
308
|
-
if v.match?(/\bcounters?\(/i)
|
|
309
|
-
v = Internal::CSS::Counters.substitute(v, Internal::CSS::Cascade.counter_values(node))
|
|
290
|
+
# The text contribution of a `::before` / `::after` pseudo-element's
|
|
291
|
+
# computed `content`. "" when the pseudo has no generated content
|
|
292
|
+
# (`none` / `normal`), or its content is purely non-text
|
|
293
|
+
# (counter/url/etc.).
|
|
294
|
+
def pseudo_content(node, pseudo)
|
|
295
|
+
decl = Internal::CSS::ComputedStyleDeclaration.new(node, pseudo_element: pseudo)
|
|
296
|
+
content_text(decl.get_property_value("content"), node)
|
|
310
297
|
end
|
|
311
298
|
|
|
312
|
-
text
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
299
|
+
# Resolve a computed `content` value to its accname text. `counter()` /
|
|
300
|
+
# `counters()` are resolved first (to quoted strings); then it scans for
|
|
301
|
+
# the text-bearing components in order — quoted strings (CSS-unescaped) and
|
|
302
|
+
# `attr(name)` (read off `node`) — and ignores everything else (`none` /
|
|
303
|
+
# `normal`, `url()`, keywords). The image `/ "alt"` syntax is covered for
|
|
304
|
+
# free: its alt string matches the quoted-string branch.
|
|
305
|
+
def content_text(value, node)
|
|
306
|
+
v = value.to_s.strip
|
|
307
|
+
return "" if v.empty? || v == "none" || v == "normal"
|
|
308
|
+
|
|
309
|
+
if v.match?(/\bcounters?\(/i)
|
|
310
|
+
v = Internal::CSS::Counters.substitute(v, Internal::CSS::Cascade.counter_values(node))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
text = +""
|
|
314
|
+
v.scan(/"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|attr\(\s*([-\w]+)\s*\)/) do
|
|
315
|
+
dq, sq, attr = Regexp.last_match.captures
|
|
316
|
+
text << (attr ? node.__internal_attribute_value__(attr).to_s : unescape_css_string(dq || sq))
|
|
317
|
+
end
|
|
318
|
+
text
|
|
316
319
|
end
|
|
317
|
-
text
|
|
318
|
-
end
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
# CSS string unescaping: `\HEX ` → the code point, `\<char>` → the char.
|
|
322
|
+
def unescape_css_string(str)
|
|
323
|
+
str.gsub(/\\(?:([0-9a-fA-F]{1,6})[ \t\n]?|(.))/) do
|
|
324
|
+
hex = Regexp.last_match(1)
|
|
325
|
+
hex ? [hex.to_i(16)].pack("U") : Regexp.last_match(2)
|
|
326
|
+
end
|
|
325
327
|
end
|
|
326
328
|
end
|
|
327
|
-
|
|
328
|
-
# Everything below compute is how it is computed, not API.
|
|
329
|
-
private_class_method :name_of
|
|
330
|
-
private_class_method :labelledby_name
|
|
331
|
-
private_class_method :native_name
|
|
332
|
-
private_class_method :child_element_name
|
|
333
|
-
private_class_method :input_native_name
|
|
334
|
-
private_class_method :placeholder_fallback
|
|
335
|
-
private_class_method :placeholder_name
|
|
336
|
-
private_class_method :label_text
|
|
337
|
-
private_class_method :associated_labels
|
|
338
|
-
private_class_method :closest_label
|
|
339
|
-
private_class_method :content_name
|
|
340
|
-
private_class_method :computed_display
|
|
341
|
-
private_class_method :pseudo_content
|
|
342
|
-
private_class_method :content_text
|
|
343
|
-
private_class_method :unescape_css_string
|
|
344
329
|
end
|
|
345
330
|
end
|
|
346
331
|
end
|