Skip to main content

script/dom/html/form_controls/
htmloptionelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::convert::TryInto;
7
8use dom_struct::dom_struct;
9use html5ever::{LocalName, Prefix, QualName, local_name, ns};
10use js::context::{JSContext, NoGC};
11use js::rust::HandleObject;
12use script_bindings::dom::{MutNullableDom, UnrootedDom};
13use script_bindings::root::Dom;
14use style::str::{split_html_space_chars, str_join};
15use stylo_dom::ElementState;
16
17use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
18use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
19use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElement_Binding::HTMLSelectElementMethods;
20use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
21use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
22use crate::dom::bindings::error::Fallible;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::reflector::DomGlobal;
25use crate::dom::bindings::root::DomRoot;
26use crate::dom::bindings::str::DOMString;
27use crate::dom::characterdata::CharacterData;
28use crate::dom::document::Document;
29use crate::dom::element::attributes::storage::AttrRef;
30use crate::dom::element::{AttributeMutation, CustomElementCreationMode, Element, ElementCreator};
31use crate::dom::html::htmlelement::HTMLElement;
32use crate::dom::html::htmlformelement::HTMLFormElement;
33use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement;
34use crate::dom::html::htmlscriptelement::HTMLScriptElement;
35use crate::dom::html::htmlselectelement::HTMLSelectElement;
36use crate::dom::iterators::ShadowIncluding;
37use crate::dom::node::virtualmethods::VirtualMethods;
38use crate::dom::node::{
39    BindContext, ChildrenMutation, CloneChildrenFlag, MoveContext, Node, NodeTraits, UnbindContext,
40};
41use crate::dom::text::Text;
42use crate::dom::types::DocumentFragment;
43use crate::dom::validation::Validatable;
44use crate::dom::validitystate::ValidationFlags;
45use crate::dom::window::Window;
46use crate::realms::enter_auto_realm;
47use crate::runtime::job_queue::MicrotaskRunnable;
48
49#[dom_struct]
50pub(crate) struct HTMLOptionElement {
51    htmlelement: HTMLElement,
52
53    /// <https://html.spec.whatwg.org/multipage/#attr-option-selected>
54    selectedness: Cell<bool>,
55
56    /// <https://html.spec.whatwg.org/multipage/#concept-option-dirtiness>
57    dirtiness: Cell<bool>,
58
59    /// <https://html.spec.whatwg.org/multipage/#cached-nearest-ancestor-select-element>
60    cached_nearest_ancestor_select: MutNullableDom<HTMLSelectElement>,
61}
62
63impl HTMLOptionElement {
64    fn new_inherited(
65        local_name: LocalName,
66        prefix: Option<Prefix>,
67        document: &Document,
68    ) -> HTMLOptionElement {
69        HTMLOptionElement {
70            htmlelement: HTMLElement::new_inherited_with_state(
71                ElementState::ENABLED,
72                local_name,
73                prefix,
74                document,
75            ),
76            selectedness: Cell::new(false),
77            dirtiness: Cell::new(false),
78            cached_nearest_ancestor_select: MutNullableDom::default(),
79        }
80    }
81
82    pub(crate) fn new(
83        cx: &mut js::context::JSContext,
84        local_name: LocalName,
85        prefix: Option<Prefix>,
86        document: &Document,
87        proto: Option<HandleObject>,
88    ) -> DomRoot<HTMLOptionElement> {
89        Node::reflect_node_with_proto(
90            cx,
91            Box::new(HTMLOptionElement::new_inherited(
92                local_name, prefix, document,
93            )),
94            document,
95            proto,
96        )
97    }
98
99    /// Set the selectedness value of this [`HTMLOptionElement`] returning true if anything
100    /// changed.
101    ///
102    /// Note: This *does not* update the containing `<select>` element.
103    pub(crate) fn set_selectedness(&self, no_gc: &NoGC, selected: bool) -> bool {
104        if selected == self.selectedness.get() {
105            return false;
106        }
107
108        self.selectedness.set(selected);
109        self.upcast::<Element>()
110            .set_state(no_gc, ElementState::CHECKED, selected);
111        // Bump the tree version so that any live HTMLCollection (e.g. selectedOptions)
112        // rooted at an ancestor invalidates its cached length and cursor.
113        self.upcast::<Node>().rev_version(no_gc);
114        true
115    }
116
117    pub(crate) fn set_dirtiness(&self, dirtiness: bool) {
118        self.dirtiness.set(dirtiness);
119    }
120
121    /// From <https://html.spec.whatwg.org/multipage/#the-select-element>:
122    ///
123    /// > If the multiple attribute is absent, whenever an option element in the select element's
124    /// > list of options has its selectedness set to true, and whenever an option element with its
125    /// > selectedness set to true is added to the select element's list of options, the user agent
126    /// > must set the selectedness of all the other option elements in its list of options to
127    /// > false.
128    fn pick_if_selected_in_select(&self, cx: &mut JSContext, select: &HTMLSelectElement) {
129        if !self.Selected() {
130            return;
131        }
132
133        debug_assert!(
134            select
135                .list_of_options(cx.no_gc())
136                .any(|option| *option == self)
137        );
138        select.pick_option(cx.no_gc(), self);
139
140        // This is not specified, but Servo needs to update the cached validity state and shadow
141        // tree of select elements when their descendants change.
142        select
143            .validity_state(cx)
144            .perform_validation_and_update(cx, ValidationFlags::all());
145        select.update_shadow_tree(cx);
146    }
147
148    fn pick_if_selected_in_cached_select(&self, cx: &mut JSContext) {
149        let select = self.cached_nearest_ancestor_select.get();
150        debug_assert!(select == self.nearest_ancestor_select());
151        if let Some(select) = select {
152            self.pick_if_selected_in_select(cx, &select);
153        }
154    }
155
156    /// <https://html.spec.whatwg.org/multipage/#concept-option-index>
157    fn index(&self, no_gc: &NoGC) -> i32 {
158        let Some(owner_select) = self.cached_nearest_ancestor_select() else {
159            return 0;
160        };
161
162        let Some(position) = owner_select.list_of_options(no_gc).position(|n| *n == self) else {
163            // An option should always be in it's owner's list of options, but it's not worth a browser panic
164            warn!("HTMLOptionElement called index_in_select at a select that did not contain it");
165            return 0;
166        };
167
168        position.try_into().unwrap_or(0)
169    }
170
171    fn update_select_validity(&self, cx: &mut JSContext) {
172        if let Some(select) = self.cached_nearest_ancestor_select() {
173            select
174                .validity_state(cx)
175                .perform_validation_and_update(cx, ValidationFlags::all());
176        }
177    }
178
179    /// <https://html.spec.whatwg.org/multipage/#concept-option-label>
180    ///
181    /// Note that this is not equivalent to <https://html.spec.whatwg.org/multipage/#dom-option-label>.
182    pub(crate) fn displayed_label(&self) -> DOMString {
183        // > The label of an option element is the value of the label content attribute, if there is one
184        // > and its value is not the empty string, or, otherwise, the value of the element's text IDL attribute.
185        let label = self
186            .upcast::<Element>()
187            .get_string_attribute(&local_name!("label"));
188
189        if label.is_empty() {
190            return self.Text();
191        }
192
193        label
194    }
195
196    /// <https://html.spec.whatwg.org/multipage/#cached-nearest-ancestor-select-element>
197    pub(crate) fn cached_nearest_ancestor_select(&self) -> Option<DomRoot<HTMLSelectElement>> {
198        self.cached_nearest_ancestor_select.get()
199    }
200
201    /// <https://html.spec.whatwg.org/multipage/#option-element-nearest-ancestor-select>
202    pub(crate) fn nearest_ancestor_select(&self) -> Option<DomRoot<HTMLSelectElement>> {
203        // Step 1. Let ancestorOptgroup be null.
204        // NOTE: We only care whether the value is non-null, so a boolean is enough
205        let mut did_see_ancestor_optgroup = false;
206
207        // Step 2. For each ancestor of option's ancestors, in reverse tree order:
208        for ancestor in self
209            .upcast::<Node>()
210            .ancestors()
211            .filter_map(DomRoot::downcast::<Element>)
212        {
213            // Step 2.1 If ancestor is a datalist, hr, or option element, then return null.
214            if matches!(
215                ancestor.local_name(),
216                &local_name!("datalist") | &local_name!("hr") | &local_name!("option")
217            ) {
218                return None;
219            }
220
221            // Step 2.2 If ancestor is an optgroup element
222            if ancestor.local_name() == &local_name!("optgroup") {
223                // Step 2.1 If ancestorOptgroup is not null, then return null.
224                if did_see_ancestor_optgroup {
225                    return None;
226                }
227
228                // Step 2.2 Set ancestorOptgroup to ancestor.
229                did_see_ancestor_optgroup = true;
230            }
231
232            // Step 2.3 If ancestor is a select, then return ancestor.
233            if let Some(select) = DomRoot::downcast::<HTMLSelectElement>(ancestor) {
234                return Some(select);
235            }
236        }
237
238        // Step 3. Return null.
239        None
240    }
241
242    /// <https://html.spec.whatwg.org/multipage/#maybe-clone-an-option-into-selectedcontent>
243    pub(crate) fn maybe_clone_an_option_into_selectedcontent(&self, cx: &mut JSContext) {
244        // Step 1. Let select be option's option element nearest ancestor select.
245        let select = self.nearest_ancestor_select();
246
247        // Step 2. If all of the following conditions are true:
248        // * select is not null;
249        // * option's selectedness is true; and
250        // * select's enabled selectedcontent is not null,
251        // * then run clone an option into a selectedcontent given option and select's enabled selectedcontent.
252        if self.selectedness.get() &&
253            let Some(selectedcontent) =
254                select.and_then(|select| select.get_enabled_selectedcontent())
255        {
256            self.clone_an_option_into_selectedcontent(cx, &selectedcontent);
257        }
258    }
259
260    /// <https://html.spec.whatwg.org/multipage/#clone-an-option-into-a-selectedcontent>
261    pub(crate) fn clone_an_option_into_selectedcontent(
262        &self,
263        cx: &mut JSContext,
264        selectedcontent: &Element,
265    ) {
266        // Step 1. Let documentFragment be a new DocumentFragment whose node document is option's node document.
267        let document_fragment = DocumentFragment::new(cx, &self.owner_document());
268
269        // Step 2. For each child of option's children:
270        for child in self.upcast::<Node>().children() {
271            // Step 2.1 Let childClone be the result of running clone given child with subtree set to true.
272            let child_clone = Node::clone(cx, &child, None, CloneChildrenFlag::CloneChildren, None);
273
274            // Step 2.2 Append childClone to documentFragment.
275            let _ = document_fragment
276                .upcast::<Node>()
277                .AppendChild(cx, &child_clone);
278        }
279
280        // Step 3. Replace all with documentFragment within selectedcontent.
281        Node::replace_all(
282            cx,
283            Some(document_fragment.upcast()),
284            selectedcontent.upcast(),
285        );
286    }
287
288    /// <https://html.spec.whatwg.org/multipage/#update-an-option's-nearest-ancestor-select>
289    fn update_nearest_ancestor_selection(
290        &self,
291        cx: &mut JSContext,
292    ) -> Vec<DomRoot<HTMLSelectElement>> {
293        // Step 1. Let oldSelect be option's cached nearest ancestor select element.
294        let old_select = self.cached_nearest_ancestor_select.get();
295
296        // Step 2. Let newSelect be option's nearest ancestor select.
297        let new_select = self.nearest_ancestor_select();
298
299        // Step 3. Set option's cached nearest ancestor select element to newSelect.
300        self.cached_nearest_ancestor_select
301            .set(new_select.as_deref());
302
303        // Step 4. If oldSelect is newSelect, then return « ».
304        if old_select == new_select {
305            return Vec::new();
306        }
307
308        // Step 5. Let result be « ».
309        let mut result = Vec::new();
310
311        // Step 6. For each select of « oldSelect, newSelect »:
312        for select in [old_select, new_select] {
313            // Step 6.1. If select is null, then continue.
314            let Some(select) = select else {
315                continue;
316            };
317
318            // Step 6.2. Let hasChanged be the result of running the selectedness setting algorithm
319            // given select.
320            let has_changed = select.selectedness_setting_algorithm(cx.no_gc());
321
322            // This is not specified, but Servo needs to update the cached validity state and shadow
323            // tree of select elements when their descendants change.
324            select
325                .validity_state(cx)
326                .perform_validation_and_update(cx, ValidationFlags::all());
327            select.update_shadow_tree(cx);
328
329            // Step 6.3. If option's selectedness is true or hasChanged is true, then append select
330            // to result.
331            if self.Selected() || has_changed {
332                result.push(select);
333            }
334        }
335
336        // Step 7. Return result.
337        result
338    }
339
340    /// <https://html.spec.whatwg.org/multipage/#ask-for-a-reset>
341    fn ask_for_a_reset(&self, cx: &mut JSContext) {
342        // Step 1. Let select be option's cached nearest ancestor select element.
343        let select = self.cached_nearest_ancestor_select.get();
344        // Step 2. If select is null, then return.
345        let Some(select) = select else {
346            return;
347        };
348        // Step 3. Run the selectedness setting algorithm given select.
349        select.selectedness_setting_algorithm(cx.no_gc());
350        // Step 4. Update a select's descendant selectedcontent elements given select.
351        select.update_a_selects_descendant_selectedcontent_elements(cx);
352
353        // Note: This is unspecified, but this is where we update the cached validity
354        // and shadow tree when things change in this option element.
355        select
356            .validity_state(cx)
357            .perform_validation_and_update(cx, ValidationFlags::all());
358        select.update_shadow_tree(cx);
359    }
360
361    /// Update the disabled state of this [`HTMLOptionElement`] based on its
362    /// disabled attribute and whether or not its owning [`HTMLOptGroupElement`]
363    /// is disabled.
364    pub(crate) fn update_disabled_state(&self, no_gc: &NoGC) {
365        let element = self.upcast::<Element>();
366        let disabled = element.has_attribute(&local_name!("disabled")) ||
367            self.is_disabled_by_parent_optgroup(no_gc);
368        element.set_disabled_state(no_gc, disabled);
369        element.set_enabled_state(no_gc, !disabled);
370    }
371
372    fn is_disabled_by_parent_optgroup(&self, no_gc: &NoGC) -> bool {
373        self.upcast::<Node>()
374            .get_parent_node_unrooted(no_gc)
375            .and_then(UnrootedDom::downcast::<HTMLOptGroupElement>)
376            .is_some_and(|optgroup| optgroup.upcast::<Element>().disabled_state())
377    }
378}
379
380impl HTMLOptionElementMethods<crate::DomTypeHolder> for HTMLOptionElement {
381    /// <https://html.spec.whatwg.org/multipage/#dom-option>
382    fn Option(
383        cx: &mut JSContext,
384        window: &Window,
385        proto: Option<HandleObject>,
386        text: DOMString,
387        value: Option<DOMString>,
388        default_selected: bool,
389        selected: bool,
390    ) -> Fallible<DomRoot<HTMLOptionElement>> {
391        let element = Element::create(
392            cx,
393            QualName::new(None, ns!(html), local_name!("option")),
394            None,
395            &window.Document(),
396            ElementCreator::ScriptCreated,
397            CustomElementCreationMode::Synchronous,
398            proto,
399        );
400
401        let option = DomRoot::downcast::<HTMLOptionElement>(element).unwrap();
402
403        if !text.is_empty() {
404            option
405                .upcast::<Node>()
406                .set_text_content_for_element(cx, Some(text))
407        }
408
409        if let Some(val) = value {
410            option.SetValue(cx, val)
411        }
412
413        option.SetDefaultSelected(cx, default_selected);
414        option.set_selectedness(cx, selected);
415        option.update_select_validity(cx);
416        Ok(option)
417    }
418
419    // https://html.spec.whatwg.org/multipage/#dom-option-disabled
420    make_bool_getter!(Disabled, "disabled");
421
422    // https://html.spec.whatwg.org/multipage/#dom-option-disabled
423    make_bool_setter!(SetDisabled, "disabled");
424
425    /// <https://html.spec.whatwg.org/multipage/#dom-option-text>
426    fn Text(&self) -> DOMString {
427        let mut content = DOMString::new();
428
429        let mut iterator = self.upcast::<Node>().traverse_preorder(ShadowIncluding::No);
430        while let Some(node) = iterator.peek() {
431            if let Some(element) = node.downcast::<Element>() {
432                let html_script = element.is::<HTMLScriptElement>();
433                let svg_script = *element.namespace() == ns!(svg) &&
434                    element.local_name() == &local_name!("script");
435                if html_script || svg_script {
436                    iterator.next_skipping_children();
437                    continue;
438                }
439            }
440
441            if node.is::<Text>() {
442                let characterdata = node.downcast::<CharacterData>().unwrap();
443                content.push_str(&characterdata.Data().str());
444            }
445
446            iterator.next();
447        }
448
449        DOMString::from(str_join(split_html_space_chars(&content.str()), " "))
450    }
451
452    /// <https://html.spec.whatwg.org/multipage/#dom-option-text>
453    fn SetText(&self, cx: &mut JSContext, value: DOMString) {
454        self.upcast::<Node>()
455            .set_text_content_for_element(cx, Some(value))
456    }
457
458    /// <https://html.spec.whatwg.org/multipage/#dom-option-form>
459    fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> {
460        self.cached_nearest_ancestor_select()
461            .and_then(|s| s.GetForm())
462    }
463
464    /// <https://html.spec.whatwg.org/multipage/#attr-option-value>
465    fn Value(&self) -> DOMString {
466        let element = self.upcast::<Element>();
467        let attr = &local_name!("value");
468        if element.has_attribute(attr) {
469            element.get_string_attribute(attr)
470        } else {
471            self.Text()
472        }
473    }
474
475    // https://html.spec.whatwg.org/multipage/#attr-option-value
476    make_setter!(SetValue, "value");
477
478    /// <https://html.spec.whatwg.org/multipage/#attr-option-label>
479    fn Label(&self) -> DOMString {
480        let element = self.upcast::<Element>();
481        let attr = &local_name!("label");
482        if element.has_attribute(attr) {
483            element.get_string_attribute(attr)
484        } else {
485            self.Text()
486        }
487    }
488
489    // https://html.spec.whatwg.org/multipage/#attr-option-label
490    make_setter!(SetLabel, "label");
491
492    // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected
493    make_bool_getter!(DefaultSelected, "selected");
494
495    // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected
496    make_bool_setter!(SetDefaultSelected, "selected");
497
498    /// <https://html.spec.whatwg.org/multipage/#dom-option-selected>
499    fn Selected(&self) -> bool {
500        self.selectedness.get()
501    }
502
503    /// <https://html.spec.whatwg.org/multipage/#dom-option-selected>
504    fn SetSelected(&self, cx: &mut JSContext, selected: bool) {
505        // Step 1. Set this's selectedness to the given value.
506        if self.set_selectedness(cx, selected) {
507            self.pick_if_selected_in_cached_select(cx);
508        }
509        // Step 2. Set this's dirtiness to true.
510        self.dirtiness.set(true);
511        // Step 3. Ask for a reset given this.
512        self.ask_for_a_reset(cx);
513    }
514
515    /// <https://html.spec.whatwg.org/multipage/#dom-option-index>
516    fn Index(&self, cx: &JSContext) -> i32 {
517        self.index(cx.no_gc())
518    }
519}
520
521impl VirtualMethods for HTMLOptionElement {
522    fn super_type(&self) -> Option<&dyn VirtualMethods> {
523        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
524    }
525
526    fn attribute_mutated(
527        &self,
528        cx: &mut js::context::JSContext,
529        attr: AttrRef<'_>,
530        mutation: AttributeMutation,
531    ) {
532        self.super_type()
533            .unwrap()
534            .attribute_mutated(cx, attr, mutation);
535        match *attr.local_name() {
536            local_name!("disabled") => {
537                self.update_disabled_state(cx.no_gc());
538                self.update_select_validity(cx);
539            },
540            local_name!("selected") => {
541                let mut selectedness_changed = false;
542                match mutation {
543                    AttributeMutation::Set(_) => {
544                        // https://html.spec.whatwg.org/multipage/#concept-option-selectedness
545                        if !self.dirtiness.get() && !self.selectedness.get() {
546                            self.set_selectedness(cx.no_gc(), true);
547                            selectedness_changed = true;
548                        }
549                    },
550                    AttributeMutation::Removed => {
551                        // https://html.spec.whatwg.org/multipage/#concept-option-selectedness
552                        if !self.dirtiness.get() && self.selectedness.get() {
553                            self.set_selectedness(cx.no_gc(), false);
554                            selectedness_changed = true;
555                        }
556                    },
557                }
558
559                if selectedness_changed {
560                    self.pick_if_selected_in_cached_select(cx);
561                    self.ask_for_a_reset(cx);
562                }
563                self.update_select_validity(cx);
564            },
565            local_name!("label") => {
566                // The label of the selected option is displayed inside the select element, so we need to repaint
567                // when it changes
568                if let Some(select_element) = self.cached_nearest_ancestor_select() {
569                    select_element.update_shadow_tree(cx);
570                }
571            },
572            _ => {},
573        }
574    }
575
576    fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
577        if let Some(super_type) = self.super_type() {
578            super_type.bind_to_tree(cx, context);
579        }
580
581        self.update_disabled_state(cx.no_gc());
582
583        // TODO: When several selected option elements are added at once, the specification doesn't
584        // say which one should win. Firefox chooses the last one and we should do the same here.
585        // This should ensure that all selected `<option>` elements outside of the inserted subtree
586        // are unselected.
587        if let Some(select) = self.nearest_ancestor_select() {
588            self.pick_if_selected_in_select(cx, &select);
589        }
590
591        self.update_nearest_ancestor_selection(cx);
592        self.update_select_validity(cx);
593    }
594
595    fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
596        self.super_type().unwrap().unbind_from_tree(cx, context);
597
598        // Step 1. Let selects be the result of update an option's nearest ancestor select given
599        // removedNode.
600        let selects = self.update_nearest_ancestor_selection(cx);
601
602        // Step 2. For each select of selects: queue a microtask to update a select's descendant
603        // selectedcontent elements given select.
604        for select in selects {
605            self.global().enqueue_microtask(
606                cx,
607                Box::new(UpdateSelectedContentMicrotask {
608                    select: select.as_traced(),
609                }),
610            );
611        }
612
613        self.update_disabled_state(cx.no_gc());
614    }
615
616    fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) {
617        if let Some(super_type) = self.super_type() {
618            super_type.children_changed(cx, mutation);
619        }
620
621        // Changing the descendants of a selected option can change it's displayed label
622        // if it does not have a label attribute
623        if !self
624            .upcast::<Element>()
625            .has_attribute(&local_name!("label")) &&
626            let Some(owner_select) = self.cached_nearest_ancestor_select() &&
627            owner_select
628                .selected_option(cx.no_gc())
629                .is_some_and(|selected_option| *self == **selected_option)
630        {
631            owner_select.update_shadow_tree(cx);
632        }
633    }
634
635    /// <https://html.spec.whatwg.org/multipage/#the-option-element:html-element-moving-steps>
636    fn moving_steps(&self, cx: &mut JSContext, context: &MoveContext) {
637        if let Some(super_type) = self.super_type() {
638            super_type.moving_steps(cx, context);
639        }
640
641        self.update_disabled_state(cx.no_gc());
642
643        // TODO: When several selected option elements are added at once, the specification doesn't
644        // say which one should win. Firefox chooses the last one and we should do the same here.
645        // This should ensure that all selected `<option>` elements outside of the moved subtree
646        // are unselected.
647        if let Some(select) = self.nearest_ancestor_select() {
648            self.pick_if_selected_in_select(cx, &select);
649        }
650
651        // Step 1. Let selects be the result of update an option's nearest ancestor select given
652        // movedNode.
653        let selects = self.update_nearest_ancestor_selection(cx);
654
655        // Step 2. For each select of selects: queue a microtask to update a select's descendant
656        // selectedcontent elements given select.
657        for select in selects {
658            self.global().enqueue_microtask(
659                cx,
660                Box::new(UpdateSelectedContentMicrotask {
661                    select: select.as_traced(),
662                }),
663            );
664        }
665    }
666}
667
668#[derive(JSTraceable, MallocSizeOf)]
669#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
670struct UpdateSelectedContentMicrotask {
671    select: Dom<HTMLSelectElement>,
672}
673
674impl MicrotaskRunnable for UpdateSelectedContentMicrotask {
675    fn handler(&self, cx: &mut JSContext) {
676        let mut realm = enter_auto_realm(cx, &*self.select);
677        let cx = &mut realm;
678        self.select
679            .update_a_selects_descendant_selectedcontent_elements(cx);
680    }
681}