Check whether an element is custom in the spec-compliant way (#35960)

* Check whether element is custom in spec-compliant way

Signed-off-by: Xiaocheng Hu <xiaochengh.work@gmail.com>

* Update tests

Signed-off-by: Xiaocheng Hu <xiaochengh.work@gmail.com>

---------

Signed-off-by: Xiaocheng Hu <xiaochengh.work@gmail.com>
This commit is contained in:
Xiaocheng Hu 2025-03-14 02:03:57 +08:00 committed by GitHub
parent ea35353e9a
commit 62d6759106
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 71 additions and 13 deletions

View file

@ -166,7 +166,7 @@ impl Attr {
MutationObserver::queue_a_mutation_record(owner.upcast::<Node>(), mutation); MutationObserver::queue_a_mutation_record(owner.upcast::<Node>(), mutation);
if owner.get_custom_element_definition().is_some() { if owner.is_custom() {
let reaction = CallbackReaction::AttributeChanged( let reaction = CallbackReaction::AttributeChanged(
name, name,
Some(old_value), Some(old_value),

View file

@ -371,6 +371,11 @@ impl Element {
CustomElementState::Uncustomized CustomElementState::Uncustomized
} }
/// <https://dom.spec.whatwg.org/#concept-element-custom>
pub(crate) fn is_custom(&self) -> bool {
self.get_custom_element_state() == CustomElementState::Custom
}
pub(crate) fn set_custom_element_definition(&self, definition: Rc<CustomElementDefinition>) { pub(crate) fn set_custom_element_definition(&self, definition: Rc<CustomElementDefinition>) {
self.ensure_rare_data().custom_element_definition = Some(definition); self.ensure_rare_data().custom_element_definition = Some(definition);
} }
@ -1580,7 +1585,7 @@ impl Element {
MutationObserver::queue_a_mutation_record(&self.node, mutation); MutationObserver::queue_a_mutation_record(&self.node, mutation);
if self.get_custom_element_definition().is_some() { if self.is_custom() {
let value = DOMString::from(&**attr.value()); let value = DOMString::from(&**attr.value());
let reaction = CallbackReaction::AttributeChanged(name, None, Some(value), namespace); let reaction = CallbackReaction::AttributeChanged(name, None, Some(value), namespace);
ScriptThread::enqueue_callback_reaction(self, reaction, None); ScriptThread::enqueue_callback_reaction(self, reaction, None);
@ -1772,9 +1777,11 @@ impl Element {
MutationObserver::queue_a_mutation_record(&self.node, mutation); MutationObserver::queue_a_mutation_record(&self.node, mutation);
let reaction = if self.is_custom() {
CallbackReaction::AttributeChanged(name, Some(old_value), None, namespace); let reaction =
ScriptThread::enqueue_callback_reaction(self, reaction, None); CallbackReaction::AttributeChanged(name, Some(old_value), None, namespace);
ScriptThread::enqueue_callback_reaction(self, reaction, None);
}
self.attrs.borrow_mut().remove(idx); self.attrs.borrow_mut().remove(idx);
attr.set_owner(None); attr.set_owner(None);
@ -2453,7 +2460,7 @@ impl ElementMethods<crate::DomTypeHolder> for Element {
} }
// Step 4. // Step 4.
if self.get_custom_element_definition().is_some() { if self.is_custom() {
let old_name = old_attr.local_name().clone(); let old_name = old_attr.local_name().clone();
let old_value = DOMString::from(&**old_attr.value()); let old_value = DOMString::from(&**old_attr.value());
let new_value = DOMString::from(&**attr.value()); let new_value = DOMString::from(&**attr.value());

View file

@ -422,7 +422,8 @@ impl Node {
pub(crate) fn as_custom_element(&self) -> Option<DomRoot<Element>> { pub(crate) fn as_custom_element(&self) -> Option<DomRoot<Element>> {
self.downcast::<Element>().and_then(|element| { self.downcast::<Element>().and_then(|element| {
if element.get_custom_element_definition().is_some() { if element.is_custom() {
assert!(element.get_custom_element_definition().is_some());
Some(DomRoot::from_ref(element)) Some(DomRoot::from_ref(element))
} else { } else {
None None
@ -2334,7 +2335,7 @@ impl Node {
.filter_map(DomRoot::downcast::<Element>) .filter_map(DomRoot::downcast::<Element>)
{ {
// Step 7.7.2, whatwg/dom#833 // Step 7.7.2, whatwg/dom#833
if descendant.get_custom_element_definition().is_some() { if descendant.is_custom() {
if descendant.is_connected() { if descendant.is_connected() {
ScriptThread::enqueue_callback_reaction( ScriptThread::enqueue_callback_reaction(
&descendant, &descendant,

View file

@ -612804,7 +612804,7 @@
] ]
], ],
"custom-element-reaction-queue.html": [ "custom-element-reaction-queue.html": [
"246b15a0af36cffa0b64f1d57e4208538b92bdd7", "eb8366f2b415894f396da613987982ae41ffe0b6",
[ [
null, null,
{} {}

View file

@ -1,4 +0,0 @@
[custom-element-reaction-queue.html]
[Upgrading a custom element must not invoke attributeChangedCallback for the attribute that is changed during upgrading]
expected: FAIL

View file

@ -83,6 +83,60 @@ test_with_window(function (contentWindow) {
assert_connected_log_entry(log[1], element); assert_connected_log_entry(log[1], element);
}, 'Upgrading a custom element must not invoke attributeChangedCallback for the attribute that is changed during upgrading'); }, 'Upgrading a custom element must not invoke attributeChangedCallback for the attribute that is changed during upgrading');
test_with_window(function (contentWindow) {
const contentDocument = contentWindow.document;
contentDocument.write('<test-element>');
const element = contentDocument.querySelector('test-element');
assert_equals(Object.getPrototypeOf(element), contentWindow.HTMLElement.prototype);
let log = [];
class TestElement extends contentWindow.HTMLElement {
constructor() {
super();
this.remove();
log.push(create_constructor_log(this));
}
connectedCallback(...args) {
log.push(create_connected_callback_log(this, ...args));
}
disconnectedCallback(...args) {
log.push(create_disconnected_callback_log(this, ...args));
}
}
contentWindow.customElements.define('test-element', TestElement);
assert_equals(Object.getPrototypeOf(element), TestElement.prototype);
assert_equals(log.length, 2);
assert_constructor_log_entry(log[0], element);
assert_connected_log_entry(log[1], element);
}, 'Upgrading a custom element must not invoke disconnectedCallback if the element is disconnected during upgrading');
test_with_window(function (contentWindow) {
const contentDocument = contentWindow.document;
const element = contentDocument.createElement('test-element');
assert_equals(Object.getPrototypeOf(element), contentWindow.HTMLElement.prototype);
let log = [];
class TestElement extends contentWindow.HTMLElement {
constructor() {
super();
contentDocument.documentElement.appendChild(this);
log.push(create_constructor_log(this));
}
connectedCallback(...args) {
log.push(create_connected_callback_log(this, ...args));
}
}
contentWindow.customElements.define('test-element', TestElement);
contentWindow.customElements.upgrade(element);
assert_equals(Object.getPrototypeOf(element), TestElement.prototype);
assert_equals(log.length, 1);
assert_constructor_log_entry(log[0], element);
}, 'Upgrading a disconnected custom element must not invoke connectedCallback if the element is connected during upgrading');
test_with_window(function (contentWindow) { test_with_window(function (contentWindow) {
const contentDocument = contentWindow.document; const contentDocument = contentWindow.document;
contentDocument.write('<test-element id="first-element">'); contentDocument.write('<test-element id="first-element">');