Implement form-associated custom elements and their ElementInternals (#31980)

* FACEs work, setFormValue test is awful so now has _mozilla backup

* 1. Impl Validatable in ElementInternals instead of HTMLElement. 2. Reuse the code in Validatable trait. 3. The form associated custom element is not a customized built-in element.

* add some comments

* support readonly attribute and complete barred from constraint validation

* Addressed the code review comments

* Updated the legacy-layout results

* Fixed the WPT failures in ElementInternals-validation.html

* Addressed the code review comments

* Review suggestions

* Fixed silly mistakes and update the test result outside elementinternals

* update the test results

---------

Co-authored-by: Patrick Shaughnessy <pshaughn@comcast.net>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
cathiechen 2024-04-11 15:17:11 +02:00 committed by GitHub
parent 2eb959a159
commit 4e4a4c0a28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 1641 additions and 619 deletions

View file

@ -12,7 +12,7 @@ use html5ever::{namespace_url, ns, LocalName, Namespace, Prefix};
use js::conversions::ToJSValConvertible;
use js::glue::UnwrapObjectStatic;
use js::jsapi::{HandleValueArray, Heap, IsCallable, IsConstructor, JSAutoRealm, JSObject};
use js::jsval::{JSVal, NullValue, ObjectValue, UndefinedValue};
use js::jsval::{BooleanValue, JSVal, NullValue, ObjectValue, UndefinedValue};
use js::rust::wrappers::{Construct1, JS_GetProperty, SameValue};
use js::rust::{HandleObject, HandleValue, MutableHandleValue};
@ -41,6 +41,7 @@ use crate::dom::domexception::{DOMErrorName, DOMException};
use crate::dom::element::Element;
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlelement::HTMLElement;
use crate::dom::htmlformelement::{FormControl, HTMLFormElement};
use crate::dom::node::{document_from_node, window_from_node, Node, ShadowIncluding};
use crate::dom::promise::Promise;
use crate::dom::window::Window;
@ -164,7 +165,8 @@ impl CustomElementRegistry {
}
/// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
/// Steps 10.3, 10.4
/// This function includes both steps 14.3 and 14.4 which add the callbacks to a map and
/// process them.
#[allow(unsafe_code)]
unsafe fn get_callbacks(&self, prototype: HandleObject) -> Fallible<LifecycleCallbacks> {
let cx = GlobalScope::get_cx();
@ -175,11 +177,34 @@ impl CustomElementRegistry {
disconnected_callback: get_callback(cx, prototype, b"disconnectedCallback\0")?,
adopted_callback: get_callback(cx, prototype, b"adoptedCallback\0")?,
attribute_changed_callback: get_callback(cx, prototype, b"attributeChangedCallback\0")?,
form_associated_callback: None,
form_disabled_callback: None,
form_reset_callback: None,
form_state_restore_callback: None,
})
}
/// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
/// Step 10.6
/// Step 14.13: Add form associated callbacks to LifecycleCallbacks
#[allow(unsafe_code)]
unsafe fn add_form_associated_callbacks(
&self,
prototype: HandleObject,
callbacks: &mut LifecycleCallbacks,
) -> ErrorResult {
let cx = self.window.get_cx();
callbacks.form_associated_callback =
get_callback(cx, prototype, b"formAssociatedCallback\0")?;
callbacks.form_reset_callback = get_callback(cx, prototype, b"formResetCallback\0")?;
callbacks.form_disabled_callback = get_callback(cx, prototype, b"formDisabledCallback\0")?;
callbacks.form_state_restore_callback =
get_callback(cx, prototype, b"formStateRestoreCallback\0")?;
Ok(())
}
#[allow(unsafe_code)]
fn get_observed_attributes(&self, constructor: HandleObject) -> Fallible<Vec<DOMString>> {
let cx = GlobalScope::get_cx();
@ -212,10 +237,75 @@ impl CustomElementRegistry {
_ => Err(Error::JSFailed),
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
/// Step 14.11: Get the value of `formAssociated`.
#[allow(unsafe_code)]
fn get_form_associated_value(&self, constructor: HandleObject) -> Fallible<bool> {
let cx = self.window.get_cx();
rooted!(in(*cx) let mut form_associated_value = UndefinedValue());
if unsafe {
!JS_GetProperty(
*cx,
constructor,
b"formAssociated\0".as_ptr() as *const _,
form_associated_value.handle_mut(),
)
} {
return Err(Error::JSFailed);
}
if form_associated_value.is_undefined() {
return Ok(false);
}
let conversion =
unsafe { FromJSValConvertible::from_jsval(*cx, form_associated_value.handle(), ()) };
match conversion {
Ok(ConversionResult::Success(flag)) => Ok(flag),
Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into())),
_ => Err(Error::JSFailed),
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
/// Step 14.7: Get `disabledFeatures` value
#[allow(unsafe_code)]
fn get_disabled_features(&self, constructor: HandleObject) -> Fallible<Vec<DOMString>> {
let cx = self.window.get_cx();
rooted!(in(*cx) let mut disabled_features = UndefinedValue());
if unsafe {
!JS_GetProperty(
*cx,
constructor,
b"disabledFeatures\0".as_ptr() as *const _,
disabled_features.handle_mut(),
)
} {
return Err(Error::JSFailed);
}
if disabled_features.is_undefined() {
return Ok(Vec::new());
}
let conversion = unsafe {
FromJSValConvertible::from_jsval(
*cx,
disabled_features.handle(),
StringificationBehavior::Default,
)
};
match conversion {
Ok(ConversionResult::Success(attributes)) => Ok(attributes),
Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into())),
_ => Err(Error::JSFailed),
}
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-customelementregistry-define>
/// Step 10.4
/// Step 14.4: Get `callbackValue` for all `callbackName` in `lifecycleCallbacks`.
#[allow(unsafe_code)]
fn get_callback(
cx: JSContext,
@ -323,7 +413,10 @@ impl CustomElementRegistryMethods for CustomElementRegistry {
// Step 9
self.element_definition_is_running.set(true);
// Steps 10.1 - 10.2
// Steps 10-13: Initialize `formAssociated`, `disableInternals`, `disableShadow`, and
// `observedAttributes` with default values, but this is done later.
// Steps 14.1 - 14.2: Get the value of the prototype.
rooted!(in(*cx) let mut prototype = UndefinedValue());
{
let _ac = JSAutoRealm::new(*cx, constructor.get());
@ -334,8 +427,12 @@ impl CustomElementRegistryMethods for CustomElementRegistry {
};
// Steps 10.3 - 10.4
// It would be easier to get all the callbacks in one pass after
// we know whether this definition is going to be form-associated,
// but the order of operations is specified and it's observable
// if one of the callback getters throws an exception.
rooted!(in(*cx) let proto_object = prototype.to_object());
let callbacks = {
let mut callbacks = {
let _ac = JSAutoRealm::new(*cx, proto_object.get());
match unsafe { self.get_callbacks(proto_object.handle()) } {
Ok(callbacks) => callbacks,
@ -346,7 +443,8 @@ impl CustomElementRegistryMethods for CustomElementRegistry {
}
};
// Step 10.5 - 10.6
// Step 14.5: Handle the case where with `attributeChangedCallback` on `lifecycleCallbacks`
// is not null.
let observed_attributes = if callbacks.attribute_changed_callback.is_some() {
let _ac = JSAutoRealm::new(*cx, constructor.get());
match self.get_observed_attributes(constructor.handle()) {
@ -360,26 +458,71 @@ impl CustomElementRegistryMethods for CustomElementRegistry {
Vec::new()
};
// Steps 14.6 - 14.10: Handle `disabledFeatures`.
let (disable_internals, disable_shadow) = {
let _ac = JSAutoRealm::new(*cx, constructor.get());
match self.get_disabled_features(constructor.handle()) {
Ok(sequence) => (
sequence.iter().any(|s| *s == "internals"),
sequence.iter().any(|s| *s == "shadow"),
),
Err(error) => {
self.element_definition_is_running.set(false);
return Err(error);
},
}
};
// Step 14.11 - 14.12: Handle `formAssociated`.
let form_associated = {
let _ac = JSAutoRealm::new(*cx, constructor.get());
match self.get_form_associated_value(constructor.handle()) {
Ok(flag) => flag,
Err(error) => {
self.element_definition_is_running.set(false);
return Err(error);
},
}
};
// Steps 14.13: Add the `formAssociated` callbacks.
if form_associated {
let _ac = JSAutoRealm::new(*cx, proto_object.get());
unsafe {
match self.add_form_associated_callbacks(proto_object.handle(), &mut callbacks) {
Err(error) => {
self.element_definition_is_running.set(false);
return Err(error);
},
Ok(()) => {},
}
}
}
self.element_definition_is_running.set(false);
// Step 11
// Step 15: Set up the new custom element definition.
let definition = Rc::new(CustomElementDefinition::new(
name.clone(),
local_name.clone(),
constructor_,
observed_attributes,
callbacks,
form_associated,
disable_internals,
disable_shadow,
));
// Step 12
// Step 16: Add definition to this CustomElementRegistry.
self.definitions
.borrow_mut()
.insert(name.clone(), definition.clone());
// Step 13
// Step 17: Let document be this CustomElementRegistry's relevant global object's
// associated Document.
let document = self.window.Document();
// Steps 14-15
// Steps 18-19: Enqueue custom elements upgrade reaction for upgrade candidates.
for candidate in document
.upcast::<Node>()
.traverse_preorder(ShadowIncluding::Yes)
@ -489,6 +632,18 @@ pub struct LifecycleCallbacks {
#[ignore_malloc_size_of = "Rc"]
attribute_changed_callback: Option<Rc<Function>>,
#[ignore_malloc_size_of = "Rc"]
form_associated_callback: Option<Rc<Function>>,
#[ignore_malloc_size_of = "Rc"]
form_reset_callback: Option<Rc<Function>>,
#[ignore_malloc_size_of = "Rc"]
form_disabled_callback: Option<Rc<Function>>,
#[ignore_malloc_size_of = "Rc"]
form_state_restore_callback: Option<Rc<Function>>,
}
#[derive(Clone, JSTraceable, MallocSizeOf)]
@ -514,6 +669,12 @@ pub struct CustomElementDefinition {
pub callbacks: LifecycleCallbacks,
pub construction_stack: DomRefCell<Vec<ConstructionStackEntry>>,
pub form_associated: bool,
pub disable_internals: bool,
pub disable_shadow: bool,
}
impl CustomElementDefinition {
@ -523,6 +684,9 @@ impl CustomElementDefinition {
constructor: Rc<CustomElementConstructor>,
observed_attributes: Vec<DOMString>,
callbacks: LifecycleCallbacks,
form_associated: bool,
disable_internals: bool,
disable_shadow: bool,
) -> CustomElementDefinition {
CustomElementDefinition {
name,
@ -531,6 +695,9 @@ impl CustomElementDefinition {
observed_attributes,
callbacks,
construction_stack: Default::default(),
form_associated: form_associated,
disable_internals: disable_internals,
disable_shadow: disable_shadow,
}
}
@ -676,7 +843,43 @@ pub fn upgrade_element(definition: Rc<CustomElementDefinition>, element: &Elemen
return;
}
// TODO Step 9: "If element is a form-associated custom element..."
// Step 9: handle with form-associated custom element
if let Some(html_element) = element.downcast::<HTMLElement>() {
if html_element.is_form_associated_custom_element() {
// We know this element is is form-associated, so we can use the implementation of
// `FormControl` for HTMLElement, which makes that assumption.
// Step 9.1: Reset the form owner of element
html_element.reset_form_owner();
if let Some(form) = html_element.form_owner() {
// Even though the tree hasn't structurally mutated,
// HTMLCollections need to be invalidated.
form.upcast::<Node>().rev_version();
// The spec tells us specifically to enqueue a formAssociated reaction
// here, but it also says to do that for resetting form owner in general,
// and we don't need two reactions.
}
// Either enabled_state or disabled_state needs to be set,
// and the possibility of a disabled fieldset ancestor needs
// to be accounted for. (In the spec, being disabled is
// a fact that's true or false about a node at a given time,
// not a flag that belongs to the node and is updated,
// so it doesn't describe this check as an action.)
element.check_disabled_attribute();
element.check_ancestors_disabled_state_for_form_control();
element.update_read_write_state_from_readonly_attribute();
// Step 9.2: If element is disabled, then enqueue a custom element callback reaction
// with element.
if element.disabled_state() {
ScriptThread::enqueue_callback_reaction(
element,
CallbackReaction::FormDisabled(true),
Some(definition.clone()),
)
}
}
}
// Step 10
element.set_custom_element_state(CustomElementState::Custom);
@ -796,6 +999,9 @@ pub enum CallbackReaction {
Disconnected,
Adopted(DomRoot<Document>, DomRoot<Document>),
AttributeChanged(LocalName, Option<DOMString>, Option<DOMString>, Namespace),
FormAssociated(Option<DomRoot<HTMLFormElement>>),
FormDisabled(bool),
FormReset,
}
/// <https://html.spec.whatwg.org/multipage/#processing-the-backup-element-queue>
@ -963,6 +1169,25 @@ impl CustomElementReactionStack {
args,
)
},
CallbackReaction::FormAssociated(form) => {
let args = vec![Heap::default()];
if let Some(form) = form {
args[0].set(ObjectValue(form.reflector().get_jsobject().get()));
} else {
args[0].set(NullValue());
}
(definition.callbacks.form_associated_callback.clone(), args)
},
CallbackReaction::FormDisabled(disabled) => {
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut disabled_value = BooleanValue(disabled));
let args = vec![Heap::default()];
args[0].set(disabled_value.get());
(definition.callbacks.form_disabled_callback.clone(), args)
},
CallbackReaction::FormReset => {
(definition.callbacks.form_reset_callback.clone(), Vec::new())
},
};
// Step 3