dom_accessibility_api/
accessible_name_and_description.rs

1use std::rc::Rc;
2
3use regex::Regex;
4use web_sys::{
5    wasm_bindgen::JsCast, window, CssStyleDeclaration, Element, HtmlFieldSetElement,
6    HtmlInputElement, HtmlLabelElement, HtmlLegendElement, HtmlOptGroupElement, HtmlSelectElement,
7    HtmlSlotElement, HtmlTableCaptionElement, HtmlTableElement, HtmlTextAreaElement, Node,
8    SvgElement, SvgTitleElement,
9};
10
11use crate::util::{
12    array_to_vec, has_any_concrete_roles, html_collection_to_vec, node_list_to_vec, query_id_refs,
13    PRESENTATION_ROLES,
14};
15
16#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
17pub enum Compute {
18    Name,
19    Description,
20}
21
22pub type GetComputedStyle = Rc<dyn Fn(&Element, Option<&str>) -> CssStyleDeclaration>;
23
24/// Options for [`compute_text_alternative`].
25#[derive(Clone, Default)]
26pub struct ComputeTextAlternativeOptions {
27    pub compute: Option<Compute>,
28
29    /// Mock `window.get_computed_style`. Needs `content`, `display` and `visibility`.
30    pub get_computed_style: Option<GetComputedStyle>,
31
32    /// Set to `true` if you want to include hidden elements in the accessible name and description computation.
33    /// Skips 2A in <https://w3c.github.io/accname/#computation-steps>.
34    ///
35    /// Defaults to `false`.
36    pub hidden: Option<bool>,
37}
38
39fn as_flat_string(s: String) -> String {
40    Regex::new(r"\s\s+")
41        .expect("Regex should be valid.")
42        .replace_all(&s, " ")
43        .to_string()
44}
45
46fn is_hidden(node: &Node, get_computed_style_implementation: GetComputedStyle) -> bool {
47    if let Some(element) = node.dyn_ref::<Element>() {
48        if element.has_attribute("hidden")
49            || element.get_attribute("aria-hidden") == Some("true".into())
50        {
51            true
52        } else {
53            let style = get_computed_style_implementation(element, None);
54
55            style
56                .get_property_value("display")
57                .expect("Computed style should have display.")
58                == "none"
59                || style
60                    .get_property_value("visibility")
61                    .expect("Computed style should have visibility.")
62                    == "hidden"
63        }
64    } else {
65        false
66    }
67}
68
69fn is_control(node: &Node) -> bool {
70    has_any_concrete_roles(node, vec!["button", "combobox", "listbox", "textbox"])
71        || has_abstract_role(node, "range")
72}
73
74fn has_abstract_role(node: &Node, role: &str) -> bool {
75    node.dyn_ref::<Element>().is_some_and(|element| match role {
76        "range" => has_any_concrete_roles(
77            element,
78            vec!["meter", "progressbar", "scrollbar", "slider", "spinbutton"],
79        ),
80        _ => unreachable!("No knowledge about abstract role '{role}'. This is likely a bug :("),
81    })
82}
83
84fn query_selector_all_subtree(element: &Element, selectors: &str) -> Vec<Element> {
85    let mut elements = node_list_to_vec(
86        element
87            .query_selector_all(selectors)
88            .expect("Element should be queried."),
89    );
90
91    for root in query_id_refs(element, "aria-owns") {
92        elements.extend(node_list_to_vec(
93            root.query_selector_all(selectors)
94                .expect("Element should be queried."),
95        ));
96    }
97
98    elements
99}
100
101fn query_selected_options(listbox: &Element) -> Vec<Element> {
102    if let Some(select_element) = listbox.dyn_ref::<HtmlSelectElement>() {
103        html_collection_to_vec(select_element.selected_options())
104    } else {
105        query_selector_all_subtree(listbox, "[aria-selected=\"true\"]")
106    }
107}
108
109fn is_marked_presentational(node: &Node) -> bool {
110    has_any_concrete_roles(node, PRESENTATION_ROLES.into())
111}
112
113fn is_native_host_language_text_alternative_element(node: &Node) -> bool {
114    // Elements specifically listed in html-aam.
115    // We don't need this for `label` or `legend` elements. Their implicit roles already allow "naming from content".
116    //
117    // https://w3c.github.io/html-aam/#table-element
118    node.is_instance_of::<HtmlTableCaptionElement>()
119}
120
121fn allows_name_from_content(node: &Node) -> bool {
122    has_any_concrete_roles(
123        node,
124        vec![
125            "button",
126            "cell",
127            "checkbox",
128            "columnheader",
129            "gridcell",
130            "heading",
131            "label",
132            "legend",
133            "link",
134            "menuitem",
135            "menuitemcheckbox",
136            "menuitemradio",
137            "option",
138            "radio",
139            "row",
140            "rowheader",
141            "switch",
142            "tab",
143            "tooltip",
144            "treeitem",
145        ],
146    )
147}
148
149// TODO: https://github.com/eps1lon/dom-accessibility-api/issues/100
150fn is_descendant_of_native_host_language_text_alternative_element(_node: &Node) -> bool {
151    false
152}
153
154fn get_value_of_textbox(element: &Element) -> String {
155    if let Some(input_element) = element.dyn_ref::<HtmlInputElement>() {
156        input_element.value()
157    } else if let Some(text_area_element) = element.dyn_ref::<HtmlTextAreaElement>() {
158        text_area_element.value()
159    } else {
160        // https://github.com/eps1lon/dom-accessibility-api/issues/4
161        element.text_content().unwrap_or("".into())
162    }
163}
164
165fn get_textual_content(declaration: CssStyleDeclaration) -> String {
166    let content = declaration
167        .get_property_value("content")
168        .expect("CssStyleDeclaration should have content.");
169    if Regex::new(r#"^["'].*["']$"#)
170        .expect("Regex should be valid.")
171        .is_match(&content)
172    {
173        (&content[1..content.len() - 1]).into()
174    } else {
175        "".into()
176    }
177}
178
179// https://html.spec.whatwg.org/multipage/forms.html#category-label
180// TODO: form-associated custom elements
181fn is_labelable_element(element: &Element) -> bool {
182    let local_name = element.local_name();
183
184    local_name == "button"
185        || (local_name == "input" && element.get_attribute("type") != Some("hidden".into()))
186        || local_name == "meter"
187        || local_name == "output"
188        || local_name == "progress"
189        || local_name == "select"
190        || local_name == "textarea"
191}
192
193// > [...], then the first such descendant in tree order is the label element's labeled control.
194// https://html.spec.whatwg.org/multipage/forms.html#labeled-control
195fn find_labelable_element(element: &Element) -> Option<Element> {
196    if is_labelable_element(element) {
197        return Some(element.clone());
198    }
199
200    for child_node in node_list_to_vec::<Node>(element.child_nodes()) {
201        if let Some(child_element) = child_node.dyn_ref::<Element>() {
202            let descendant_labelable_element = find_labelable_element(child_element);
203            if let Some(descendant_labelable_element) = descendant_labelable_element {
204                return Some(descendant_labelable_element);
205            }
206        }
207    }
208
209    None
210}
211
212// Polyfill of HTMLLabelElement.control
213// https://html.spec.whatwg.org/multipage/forms.html#labeled-control
214fn get_control_of_label(label: &HtmlLabelElement) -> Option<Element> {
215    if let Some(control) = label.control() {
216        return Some(control.into());
217    }
218
219    let html_for = label.get_attribute("for");
220    if let Some(html_for) = html_for {
221        return label
222            .owner_document()
223            .expect("Owner document should exist.")
224            .get_element_by_id(&html_for);
225    }
226
227    find_labelable_element(label)
228}
229
230// Polyfill of HTMLInputElement.labels
231// https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/labels
232fn get_labels(element: &Element) -> Vec<HtmlLabelElement> {
233    if let Some(input_element) = element.dyn_ref::<HtmlInputElement>() {
234        input_element
235            .labels()
236            .map(node_list_to_vec)
237            .unwrap_or_default()
238    } else if !is_labelable_element(element) {
239        vec![]
240    } else {
241        let document = element
242            .owner_document()
243            .expect("Owner document should exist.");
244        node_list_to_vec(
245            document
246                .query_selector_all("label")
247                .expect("Document should be queried."),
248        )
249        .into_iter()
250        .filter(|label| get_control_of_label(label).is_some_and(|label| label == *element))
251        .collect()
252    }
253}
254
255// Gets the contents of a slot used for computing the accname.
256fn get_slot_contents(slot: &HtmlSlotElement) -> Vec<Node> {
257    // Computing the accessible name for elements containing slots is not currently defined in the spec.
258    // This implementation reflects the behavior of NVDA 2020.2/Firefox 81 and iOS VoiceOver/Safari 13.6.
259
260    let assigned_nodes = slot.assigned_nodes();
261    if assigned_nodes.length() == 0 {
262        // If no nodes are assigned to the slot, it displays the default content.
263        node_list_to_vec(slot.child_nodes())
264    } else {
265        array_to_vec(assigned_nodes)
266    }
267}
268
269struct ComputeTextAlternativeContext {
270    is_embedded_in_label: bool,
271    is_referenced: bool,
272    recursion: bool,
273}
274
275struct ComputeMiscTextAlternativeContext {
276    is_embedded_in_label: bool,
277    #[expect(dead_code)]
278    is_referenced: bool,
279}
280
281// Implements <https://w3c.github.io/accname/#mapping_additional_nd_te>.
282pub fn compute_text_alternative(root: &Element, options: ComputeTextAlternativeOptions) -> String {
283    let mut consulted_nodes: Vec<Node> = vec![];
284
285    let compute = options.compute.unwrap_or(Compute::Name);
286    let uncached_get_computed_style = options.get_computed_style.unwrap_or_else(|| {
287        Rc::new(|element, pseudo_elt| {
288            let window = window().expect("Window should exist.");
289
290            if let Some(pseudo_elt) = pseudo_elt {
291                window.get_computed_style_with_pseudo_elt(element, pseudo_elt)
292            } else {
293                window.get_computed_style(element)
294            }
295            .expect("Element should be valid.")
296            .expect("Computed style should exist.")
297        })
298    });
299    let hidden = options.hidden.unwrap_or(false);
300
301    let get_computed_style: GetComputedStyle = Rc::new({
302        let uncached_get_computed_style = uncached_get_computed_style.clone();
303
304        move |element, pseudo_elt| {
305            // TODO: cache
306            uncached_get_computed_style(element, pseudo_elt)
307        }
308    });
309
310    // 2F.i
311    fn compute_misc_text_alternative(
312        compute: Compute,
313        hidden: bool,
314        uncached_get_computed_style: GetComputedStyle,
315        get_computed_style: GetComputedStyle,
316        consulted_nodes: &mut Vec<Node>,
317        node: &Node,
318        context: ComputeMiscTextAlternativeContext,
319    ) -> String {
320        let mut accumalated_text = "".to_string();
321
322        if let Some(element) = node.dyn_ref::<Element>() {
323            let pseudo_before = uncached_get_computed_style(element, Some("::before"));
324            let before_content = get_textual_content(pseudo_before);
325            accumalated_text = format!("{before_content} {accumalated_text}");
326        }
327
328        // FIXME: Including aria-owns is not defined in the spec, but it is required in the web-platform-test.
329        let child_nodes = node
330            .dyn_ref::<HtmlSlotElement>()
331            .map(get_slot_contents)
332            .unwrap_or_else(|| {
333                let mut nodes = node_list_to_vec(node.child_nodes());
334                nodes.extend(
335                    query_id_refs(node, "aria-owns")
336                        .into_iter()
337                        .map(|element| element.into()),
338                );
339                nodes
340            });
341        for child in child_nodes {
342            let result = inner_compute_text_alternative(
343                compute,
344                hidden,
345                uncached_get_computed_style.clone(),
346                get_computed_style.clone(),
347                consulted_nodes,
348                &child,
349                ComputeTextAlternativeContext {
350                    is_embedded_in_label: context.is_embedded_in_label,
351                    is_referenced: false,
352                    recursion: true,
353                },
354            );
355            // TODO: Unclear why display affects delimiter, see https://github.com/w3c/accname/issues/3.
356            let display = if let Some(element) = child.dyn_ref::<Element>() {
357                get_computed_style(element, None)
358                    .get_property_value("display")
359                    .expect("Computed style should have display.")
360            } else {
361                "inline".into()
362            };
363            let separator = if display != "inline" { " " } else { "" };
364            // Trailing separator for WPT tests.
365            accumalated_text = format!("{accumalated_text}{separator}{result}{separator}");
366        }
367
368        if let Some(element) = node.dyn_ref::<Element>() {
369            let pseudo_after = uncached_get_computed_style(element, Some("::after"));
370            let after_content = get_textual_content(pseudo_after);
371            accumalated_text = format!("{accumalated_text} {after_content}");
372        }
373
374        return accumalated_text.trim().into();
375    }
376
377    fn use_attribute(
378        consulted_nodes: &mut Vec<Node>,
379        element: &Element,
380        attribute_name: &str,
381    ) -> Option<String> {
382        if let Some(attribute) = element.get_attribute_node(attribute_name) {
383            let value = attribute.value();
384            if !consulted_nodes.contains(&attribute) && !value.trim().is_empty() {
385                consulted_nodes.push(attribute.into());
386                return Some(value);
387            }
388        }
389
390        None
391    }
392
393    fn compute_tooltip_attribute_value(
394        consulted_nodes: &mut Vec<Node>,
395        node: &Node,
396    ) -> Option<String> {
397        node.dyn_ref::<Element>()
398            .and_then(|element| use_attribute(consulted_nodes, element, "title"))
399    }
400
401    fn compute_element_text_alternative(
402        compute: Compute,
403        hidden: bool,
404        uncached_get_computed_style: GetComputedStyle,
405        get_computed_style: GetComputedStyle,
406        consulted_nodes: &mut Vec<Node>,
407        node: &Node,
408    ) -> Option<String> {
409        if let Some(element) = node.dyn_ref::<Element>() {
410            if element.is_instance_of::<HtmlFieldSetElement>() {
411                // https://w3c.github.io/html-aam/#fieldset-and-legend-elements
412
413                consulted_nodes.push(node.clone());
414
415                let children = element.child_nodes();
416                for i in 0..children.length() {
417                    let child = children.item(i).expect("Item should exist.");
418
419                    if child.is_instance_of::<HtmlLegendElement>() {
420                        return Some(inner_compute_text_alternative(
421                            compute,
422                            hidden,
423                            uncached_get_computed_style,
424                            get_computed_style,
425                            consulted_nodes,
426                            &child,
427                            ComputeTextAlternativeContext {
428                                is_embedded_in_label: false,
429                                is_referenced: false,
430                                recursion: false,
431                            },
432                        ));
433                    }
434                }
435            } else if element.is_instance_of::<HtmlTableElement>() {
436                // https://w3c.github.io/html-aam/#table-element
437
438                consulted_nodes.push(node.clone());
439
440                let children = element.child_nodes();
441                for i in 0..children.length() {
442                    let child = children.item(i).expect("Item should exist.");
443
444                    if child.is_instance_of::<HtmlTableCaptionElement>() {
445                        return Some(inner_compute_text_alternative(
446                            compute,
447                            hidden,
448                            uncached_get_computed_style,
449                            get_computed_style,
450                            consulted_nodes,
451                            &child,
452                            ComputeTextAlternativeContext {
453                                is_embedded_in_label: false,
454                                is_referenced: false,
455                                recursion: false,
456                            },
457                        ));
458                    }
459                }
460            } else if element.is_instance_of::<SvgElement>() {
461                // https://www.w3.org/TR/svg-aam-1.0/
462
463                consulted_nodes.push(node.clone());
464
465                let children = element.child_nodes();
466                for i in 0..children.length() {
467                    let child = children.item(i).expect("Item should exist.");
468
469                    if child.is_instance_of::<SvgTitleElement>() {
470                        return child.text_content();
471                    }
472                }
473            } else if element.local_name() == "img" || element.local_name() == "area" {
474                // https://w3c.github.io/html-aam/#area-element
475                // https://w3c.github.io/html-aam/#img-element
476                if let Some(name_from_alt) = use_attribute(consulted_nodes, element, "alt") {
477                    return Some(name_from_alt);
478                }
479            } else if element.is_instance_of::<HtmlOptGroupElement>() {
480                if let Some(name_from_label) = use_attribute(consulted_nodes, element, "label") {
481                    return Some(name_from_label);
482                }
483            }
484
485            if let Some(input_element) = element.dyn_ref::<HtmlInputElement>() {
486                if input_element.type_() == "button"
487                    || input_element.type_() == "submit"
488                    || input_element.type_() == "reset"
489                {
490                    // https://w3c.github.io/html-aam/#input-type-text-input-type-password-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-description-computation
491                    if let Some(name_from_value) = use_attribute(consulted_nodes, element, "value")
492                    {
493                        return Some(name_from_value);
494                    }
495
496                    // TODO: l10n
497                    if input_element.type_() == "submit" {
498                        return Some("Submit".into());
499                    }
500                    // TODO: l10n
501                    if input_element.type_() == "reset" {
502                        return Some("Reset".into());
503                    }
504                }
505            }
506
507            let labels = get_labels(element);
508            if !labels.is_empty() {
509                consulted_nodes.push(node.clone());
510
511                return Some(
512                    labels
513                        .into_iter()
514                        .map(|element| {
515                            inner_compute_text_alternative(
516                                compute,
517                                hidden,
518                                uncached_get_computed_style.clone(),
519                                get_computed_style.clone(),
520                                consulted_nodes,
521                                &element,
522                                ComputeTextAlternativeContext {
523                                    is_embedded_in_label: true,
524                                    is_referenced: false,
525                                    recursion: true,
526                                },
527                            )
528                        })
529                        .filter(|label| !label.is_empty())
530                        .collect::<Vec<_>>()
531                        .join(" "),
532                );
533            }
534
535            // https://w3c.github.io/html-aam/#input-type-image-accessible-name-computation
536            // TODO: WPT test consider label elements but html-aam does not mention them.
537            // We follow existing implementations over spec.
538            if let Some(input_element) = node.dyn_ref::<HtmlInputElement>() {
539                if input_element.type_() == "image" {
540                    let name_for_alt = use_attribute(consulted_nodes, input_element, "alt");
541                    if let Some(name_for_alt) = name_for_alt {
542                        return Some(name_for_alt);
543                    }
544
545                    let name_for_title = use_attribute(consulted_nodes, input_element, "title");
546                    if let Some(name_for_alt) = name_for_title {
547                        return Some(name_for_alt);
548                    }
549
550                    // TODO: l10n
551                    return Some("Submit Query".into());
552                }
553            }
554
555            if has_any_concrete_roles(node, vec!["button"]) {
556                // https://www.w3.org/TR/html-aam-1.0/#button-element
557                let name_from_sub_tree = compute_misc_text_alternative(
558                    compute,
559                    hidden,
560                    uncached_get_computed_style,
561                    get_computed_style,
562                    consulted_nodes,
563                    node,
564                    ComputeMiscTextAlternativeContext {
565                        is_embedded_in_label: false,
566                        is_referenced: false,
567                    },
568                );
569                if !name_from_sub_tree.is_empty() {
570                    return Some(name_from_sub_tree);
571                }
572            }
573        }
574
575        None
576    }
577
578    fn inner_compute_text_alternative(
579        compute: Compute,
580        hidden: bool,
581        uncached_get_computed_style: GetComputedStyle,
582        get_computed_style: GetComputedStyle,
583        consulted_nodes: &mut Vec<Node>,
584        current: &Node,
585        context: ComputeTextAlternativeContext,
586    ) -> String {
587        if consulted_nodes.contains(current) {
588            return "".into();
589        }
590
591        // 2A
592        if !hidden && is_hidden(current, get_computed_style.clone()) && !context.is_referenced {
593            consulted_nodes.push(current.clone());
594            return "".into();
595        }
596
597        // 2B
598        if let Some(current) = current.dyn_ref::<Element>() {
599            if let Some(label_attribute_node) = current.get_attribute_node("aria-labelledby") {
600                // TODO: Do we generally need to block query IdRefs of attributes we have already consulted?
601                let label_elements = if !consulted_nodes.contains(&label_attribute_node) {
602                    query_id_refs(current, "aria-labelledby")
603                } else {
604                    vec![]
605                };
606
607                if compute == Compute::Name && !context.is_referenced && !label_elements.is_empty()
608                {
609                    consulted_nodes.push(label_attribute_node.unchecked_into::<Node>());
610
611                    return label_elements
612                        .into_iter()
613                        .map(move |element| {
614                            // TODO: Chrome will consider repeated values i.e. use a node multiple times while we'll bail out in computeTextAlternative.
615                            inner_compute_text_alternative(
616                                compute,
617                                hidden,
618                                uncached_get_computed_style.clone(),
619                                get_computed_style.clone(),
620                                consulted_nodes,
621                                &element,
622                                ComputeTextAlternativeContext {
623                                    is_embedded_in_label: context.is_embedded_in_label,
624                                    is_referenced: true,
625                                    // This isn't recursion as specified, otherwise we would skip `aria-label` in
626                                    // <input id="myself" aria-label="foo" aria-labelledby="myself" />
627                                    recursion: false,
628                                },
629                            )
630                        })
631                        .collect::<Vec<_>>()
632                        .join(" ");
633                }
634            }
635        }
636
637        // 2C
638        // Changed from the spec in anticipation of https://github.com/w3c/accname/issues/64.
639        // Spec says we should only consider skipping if we have a non-empty label.
640        let skip_to_step_2e = context.recursion && is_control(current) && compute == Compute::Name;
641        if !skip_to_step_2e {
642            let aria_label = current
643                .dyn_ref::<Element>()
644                .and_then(|current| current.get_attribute("aria-label"))
645                .unwrap_or_default()
646                .trim()
647                .to_string();
648            if !aria_label.is_empty() && compute == Compute::Name {
649                consulted_nodes.push(current.clone());
650                return aria_label;
651            }
652
653            // 2D
654            if !is_marked_presentational(current) {
655                if let Some(element_text_alternative) = compute_element_text_alternative(
656                    compute,
657                    hidden,
658                    uncached_get_computed_style.clone(),
659                    get_computed_style.clone(),
660                    consulted_nodes,
661                    current,
662                ) {
663                    consulted_nodes.push(current.clone());
664                    return element_text_alternative;
665                }
666            }
667        }
668
669        // Special casing, cheating to make tests pass.
670        // https://github.com/w3c/accname/issues/67
671        if has_any_concrete_roles(current, vec!["menu"]) {
672            consulted_nodes.push(current.clone());
673            return "".into();
674        }
675
676        // 2E
677        if skip_to_step_2e || context.is_embedded_in_label || context.is_referenced {
678            if has_any_concrete_roles(current, vec!["combobox", "listbox"]) {
679                consulted_nodes.push(current.clone());
680
681                let selected_options = query_selected_options(
682                    current
683                        .dyn_ref::<Element>()
684                        .expect("Node should be an Element."),
685                );
686                if selected_options.is_empty() {
687                    // Defined per test `name_heading_combobox`.
688                    return current
689                        .dyn_ref::<HtmlInputElement>()
690                        .map(|input_element| input_element.value())
691                        .unwrap_or("".into());
692                }
693                return selected_options
694                    .iter()
695                    .map(|selected_option| {
696                        inner_compute_text_alternative(
697                            compute,
698                            hidden,
699                            uncached_get_computed_style.clone(),
700                            get_computed_style.clone(),
701                            consulted_nodes,
702                            selected_option,
703                            ComputeTextAlternativeContext {
704                                is_embedded_in_label: context.is_embedded_in_label,
705                                is_referenced: false,
706                                recursion: true,
707                            },
708                        )
709                    })
710                    .collect::<Vec<_>>()
711                    .join(" ");
712            }
713            if has_abstract_role(current, "range") {
714                consulted_nodes.push(current.clone());
715                let element = current
716                    .dyn_ref::<Element>()
717                    .expect("Node should be an Element.");
718                if element.has_attribute("aria-valuetext") {
719                    return element
720                        .get_attribute("aria-valuetext")
721                        .expect("Attribute should exist.");
722                }
723                if element.has_attribute("aria-valuenow") {
724                    return element
725                        .get_attribute("aria-valuenow")
726                        .expect("Attribute should exist.");
727                }
728                return element.get_attribute("value").unwrap_or("".into());
729            }
730            if has_any_concrete_roles(current, vec!["textbox"]) {
731                consulted_nodes.push(current.clone());
732
733                return get_value_of_textbox(
734                    current
735                        .dyn_ref::<Element>()
736                        .expect("Node should be an Element."),
737                );
738            }
739        }
740
741        // 2F
742        if allows_name_from_content(current)
743            || (current.is_instance_of::<Element>() && context.is_referenced)
744            || is_native_host_language_text_alternative_element(current)
745            || is_descendant_of_native_host_language_text_alternative_element(current)
746        {
747            let accumulated_text_2f = compute_misc_text_alternative(
748                compute,
749                hidden,
750                uncached_get_computed_style.clone(),
751                get_computed_style.clone(),
752                consulted_nodes,
753                current,
754                ComputeMiscTextAlternativeContext {
755                    is_embedded_in_label: context.is_embedded_in_label,
756                    is_referenced: false,
757                },
758            );
759            if !accumulated_text_2f.is_empty() {
760                consulted_nodes.push(current.clone());
761                return accumulated_text_2f;
762            }
763        }
764
765        if current.node_type() == Node::TEXT_NODE {
766            consulted_nodes.push(current.clone());
767            return current.text_content().unwrap_or("".into());
768        }
769
770        if context.recursion {
771            consulted_nodes.push(current.clone());
772            return compute_misc_text_alternative(
773                compute,
774                hidden,
775                uncached_get_computed_style,
776                get_computed_style,
777                consulted_nodes,
778                current,
779                ComputeMiscTextAlternativeContext {
780                    is_embedded_in_label: context.is_embedded_in_label,
781                    is_referenced: false,
782                },
783            );
784        }
785
786        let tooltip_attribute_value = compute_tooltip_attribute_value(consulted_nodes, current);
787        if let Some(tooltip_attribute_value) = tooltip_attribute_value {
788            consulted_nodes.push(current.clone());
789            return tooltip_attribute_value;
790        }
791
792        // TODO: should this be reachable?
793        consulted_nodes.push(current.clone());
794        "".into()
795    }
796
797    as_flat_string(inner_compute_text_alternative(
798        compute,
799        hidden,
800        uncached_get_computed_style,
801        get_computed_style,
802        &mut consulted_nodes,
803        root,
804        ComputeTextAlternativeContext {
805            is_embedded_in_label: false,
806            // By spec `compute_accessible_description starts with the referenced elements as roots.
807            is_referenced: compute == Compute::Description,
808            recursion: false,
809        },
810    ))
811}