diff --git a/dom/html/ElementInternals.cpp b/dom/html/ElementInternals.cpp
index 3c0dfe0fe976..dd0221825034 100644
--- a/dom/html/ElementInternals.cpp
+++ b/dom/html/ElementInternals.cpp
@@ -24,6 +24,7 @@ NS_IMPL_CYCLE_COLLECTING_RELEASE(ElementInternals)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ElementInternals)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsIFormControl)
+ NS_INTERFACE_MAP_ENTRY(nsIConstraintValidation)
NS_INTERFACE_MAP_END
ElementInternals::ElementInternals(HTMLElement* aTarget)
@@ -123,6 +124,16 @@ HTMLFormElement* ElementInternals::GetForm(ErrorResult& aRv) const {
return GetForm();
}
+// https://html.spec.whatwg.org/#dom-elementinternals-willvalidate
+bool ElementInternals::GetWillValidate(ErrorResult& aRv) const {
+ if (!mTarget || !mTarget->IsFormAssociatedElement()) {
+ aRv.ThrowNotSupportedError(
+ "Target element is not a form-associated custom element");
+ return false;
+ }
+ return WillValidate();
+}
+
// https://html.spec.whatwg.org/#dom-elementinternals-labels
already_AddRefed ElementInternals::GetLabels(
ErrorResult& aRv) const {
@@ -188,6 +199,16 @@ void ElementInternals::UpdateFormOwner() {
}
}
+void ElementInternals::UpdateBarredFromConstraintValidation() {
+ if (mTarget) {
+ MOZ_ASSERT(mTarget->IsFormAssociatedElement());
+ SetBarredFromConstraintValidation(
+ mTarget->HasAttr(nsGkAtoms::readonly) ||
+ mTarget->HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR) ||
+ mTarget->IsDisabled());
+ }
+}
+
void ElementInternals::Unlink() {
if (mForm) {
// Don't notify, since we're being destroyed in any case.
diff --git a/dom/html/ElementInternals.h b/dom/html/ElementInternals.h
index a081db7dc4d0..a283e46af381 100644
--- a/dom/html/ElementInternals.h
+++ b/dom/html/ElementInternals.h
@@ -10,6 +10,7 @@
#include "js/TypeDecls.h"
#include "mozilla/dom/ElementInternalsBinding.h"
#include "nsCycleCollectionParticipant.h"
+#include "nsIConstraintValidation.h"
#include "nsIFormControl.h"
#include "nsWrapperCache.h"
@@ -26,10 +27,13 @@ class HTMLFieldSetElement;
class HTMLFormElement;
class ShadowRoot;
-class ElementInternals final : public nsIFormControl, public nsWrapperCache {
+class ElementInternals final : public nsIFormControl,
+ public nsIConstraintValidation,
+ public nsWrapperCache {
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
- NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ElementInternals)
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(ElementInternals,
+ nsIFormControl)
explicit ElementInternals(HTMLElement* aTarget);
@@ -44,6 +48,7 @@ class ElementInternals final : public nsIFormControl, public nsWrapperCache {
const Optional>& aState,
ErrorResult& aRv);
mozilla::dom::HTMLFormElement* GetForm(ErrorResult& aRv) const;
+ bool GetWillValidate(ErrorResult& aRv) const;
already_AddRefed GetLabels(ErrorResult& aRv) const;
// nsIFormControl
@@ -62,6 +67,7 @@ class ElementInternals final : public nsIFormControl, public nsWrapperCache {
}
void UpdateFormOwner();
+ void UpdateBarredFromConstraintValidation();
void Unlink();
diff --git a/dom/html/HTMLElement.cpp b/dom/html/HTMLElement.cpp
index f63e952c60a7..a35a3b346d76 100644
--- a/dom/html/HTMLElement.cpp
+++ b/dom/html/HTMLElement.cpp
@@ -25,6 +25,7 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLElement, nsGenericHTMLFormElement)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLElement)
NS_INTERFACE_MAP_ENTRY_TEAROFF(nsIFormControl, GetElementInternals())
+ NS_INTERFACE_MAP_ENTRY_TEAROFF(nsIConstraintValidation, GetElementInternals())
NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLFormElement)
NS_IMPL_ADDREF_INHERITED(HTMLElement, nsGenericHTMLFormElement)
@@ -37,6 +38,20 @@ JSObject* HTMLElement::WrapNode(JSContext* aCx,
return dom::HTMLElement_Binding::Wrap(aCx, this, aGivenProto);
}
+nsresult HTMLElement::BindToTree(BindContext& aContext, nsINode& aParent) {
+ nsresult rv = nsGenericHTMLFormElement::BindToTree(aContext, aParent);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ UpdateBarredFromConstraintValidation();
+ return rv;
+}
+
+void HTMLElement::UnbindFromTree(bool aNullParent) {
+ nsGenericHTMLFormElement::UnbindFromTree(aNullParent);
+
+ UpdateBarredFromConstraintValidation();
+}
+
void HTMLElement::SetCustomElementDefinition(
CustomElementDefinition* aDefinition) {
nsGenericHTMLFormElement::SetCustomElementDefinition(aDefinition);
@@ -161,6 +176,7 @@ void HTMLElement::UpdateFormOwner() {
}
UpdateFieldSet(true);
UpdateDisabledState(true);
+ UpdateBarredFromConstraintValidation();
}
nsresult HTMLElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
@@ -168,8 +184,14 @@ nsresult HTMLElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
const nsAttrValue* aOldValue,
nsIPrincipal* aMaybeScriptedPrincipal,
bool aNotify) {
- if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled) {
- UpdateDisabledState(aNotify);
+ if (aNameSpaceID == kNameSpaceID_None &&
+ (aName == nsGkAtoms::disabled || aName == nsGkAtoms::readonly)) {
+ if (aName == nsGkAtoms::disabled) {
+ // This *has* to be called *before* validity state check because
+ // UpdateBarredFromConstraintValidation depend on our disabled state.
+ UpdateDisabledState(aNotify);
+ }
+ UpdateBarredFromConstraintValidation();
}
return nsGenericHTMLFormElement::AfterSetAttr(
@@ -239,6 +261,14 @@ bool HTMLElement::IsFormAssociatedElement() const {
return isFormAssociatedCustomElement;
}
+void HTMLElement::FieldSetDisabledChanged(bool aNotify) {
+ // This *has* to be called *before* UpdateBarredFromConstraintValidation
+ // because this function depend on our disabled state.
+ nsGenericHTMLFormElement::FieldSetDisabledChanged(aNotify);
+
+ UpdateBarredFromConstraintValidation();
+}
+
ElementInternals* HTMLElement::GetElementInternals() const {
CustomElementData* data = GetCustomElementData();
if (!data || !data->IsFormAssociated()) {
@@ -251,6 +281,15 @@ ElementInternals* HTMLElement::GetElementInternals() const {
return data->GetElementInternals();
}
+void HTMLElement::UpdateBarredFromConstraintValidation() {
+ CustomElementData* data = GetCustomElementData();
+ if (data && data->IsFormAssociated()) {
+ ElementInternals* internals = data->GetElementInternals();
+ MOZ_ASSERT(internals);
+ internals->UpdateBarredFromConstraintValidation();
+ }
+}
+
} // namespace mozilla::dom
// Here, we expand 'NS_IMPL_NS_NEW_HTML_ELEMENT()' by hand.
diff --git a/dom/html/HTMLElement.h b/dom/html/HTMLElement.h
index cb7c008156b7..2e23d5cc8c18 100644
--- a/dom/html/HTMLElement.h
+++ b/dom/html/HTMLElement.h
@@ -23,6 +23,10 @@ class HTMLElement final : public nsGenericHTMLFormElement {
// nsINode
nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
+ // nsIContent
+ nsresult BindToTree(BindContext&, nsINode& aParent) override;
+ void UnbindFromTree(bool aNullParent = true) override;
+
// Element
void SetCustomElementDefinition(
CustomElementDefinition* aDefinition) override;
@@ -36,6 +40,7 @@ class HTMLElement final : public nsGenericHTMLFormElement {
// nsGenericHTMLFormElement
bool IsFormAssociatedElement() const override;
void AfterClearForm(bool aUnbindOrDelete) override;
+ void FieldSetDisabledChanged(bool aNotify) override;
void UpdateFormOwner();
@@ -61,6 +66,8 @@ class HTMLElement final : public nsGenericHTMLFormElement {
void UpdateDisabledState(bool aNotify) override;
void UpdateFormOwner(bool aBindToTree, Element* aFormIdElement) override;
+ void UpdateBarredFromConstraintValidation();
+
ElementInternals* GetElementInternals() const;
};
diff --git a/dom/webidl/ElementInternals.webidl b/dom/webidl/ElementInternals.webidl
index 40eeb74cd8b4..44c65e78bb96 100644
--- a/dom/webidl/ElementInternals.webidl
+++ b/dom/webidl/ElementInternals.webidl
@@ -20,6 +20,9 @@ interface ElementInternals {
[Pref="dom.webcomponents.formAssociatedCustomElement.enabled", Throws]
readonly attribute HTMLFormElement? form;
+ [Pref="dom.webcomponents.formAssociatedCustomElement.enabled", Throws]
+ readonly attribute boolean willValidate;
+
[Pref="dom.webcomponents.formAssociatedCustomElement.enabled", Throws]
readonly attribute NodeList labels;
};
diff --git a/testing/web-platform/meta/custom-elements/form-associated/ElementInternals-validation.html.ini b/testing/web-platform/meta/custom-elements/form-associated/ElementInternals-validation.html.ini
index d7bdad331752..d17749860e47 100644
--- a/testing/web-platform/meta/custom-elements/form-associated/ElementInternals-validation.html.ini
+++ b/testing/web-platform/meta/custom-elements/form-associated/ElementInternals-validation.html.ini
@@ -11,9 +11,6 @@
[Custom control affects validation at the owner form]
expected: FAIL
- [willValidate]
- expected: FAIL
-
["anchor" argument of setValidity()]
expected: FAIL
diff --git a/testing/web-platform/meta/html/dom/idlharness.https.html.ini b/testing/web-platform/meta/html/dom/idlharness.https.html.ini
index 39fc70a8302e..3214280a351c 100644
--- a/testing/web-platform/meta/html/dom/idlharness.https.html.ini
+++ b/testing/web-platform/meta/html/dom/idlharness.https.html.ini
@@ -372,7 +372,8 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu
expected: FAIL
[ElementInternals interface: attribute willValidate]
- expected: FAIL
+ expected:
+ if release_or_beta: FAIL
[OffscreenCanvasRenderingContext2D interface: operation moveTo(unrestricted double, unrestricted double)]
expected: FAIL
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html
index f67e96465bd6..6b2ef8739795 100644
--- a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html
@@ -16,6 +16,15 @@ class MyControl extends HTMLElement {
}
customElements.define('my-control', MyControl);
+class NotFormAssociatedElement extends HTMLElement {
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+}
+customElements.define('not-form-associated-element', NotFormAssociatedElement);
+
test(() => {
const control = new MyControl();
assert_true(control.i.willValidate, 'default value is true');
@@ -23,18 +32,64 @@ test(() => {
const datalist = document.createElement('datalist');
datalist.appendChild(control);
assert_false(control.i.willValidate, 'false in DATALIST');
+ datalist.removeChild(control);
+ assert_true(control.i.willValidate, 'remove from DATALIST');
const fieldset = document.createElement('fieldset');
- fieldset.appendChild(control);
- assert_true(control.i.willValidate, 'In enabled FIELDSET');
fieldset.disabled = true;
- assert_false(control.i.willValidate, 'In disabled FIELDSET');
+ fieldset.appendChild(control);
+ assert_false(control.i.willValidate, 'append to disabled FIELDSET');
+ fieldset.disabled = false;
+ assert_true(control.i.willValidate, 'FIELDSET becomes enabled');
+ fieldset.disabled = true;
+ assert_false(control.i.willValidate, 'FIELDSET becomes disabled');
fieldset.removeChild(control);
+ assert_true(control.i.willValidate, 'remove from disabled FIELDSET');
control.setAttribute('disabled', '');
assert_false(control.i.willValidate, 'with disabled attribute');
+ control.removeAttribute('disabled');
+ assert_true(control.i.willValidate, 'without disabled attribute');
+
+ control.setAttribute('readonly', '');
+ assert_false(control.i.willValidate, 'with readonly attribute');
+ control.removeAttribute('readonly');
+ assert_true(control.i.willValidate, 'without readonly attribute');
}, 'willValidate');
+test(() => {
+ const container = document.getElementById("container");
+ container.innerHTML = '' +
+ '' +
+ '' +
+ '' +
+ '';
+
+ class WillBeDefined extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+ }
+ customElements.define('will-be-defined', WillBeDefined);
+ customElements.upgrade(container);
+
+ const controls = document.querySelectorAll('will-be-defined');
+ assert_true(controls[0].i.willValidate, 'default value');
+ assert_false(controls[1].i.willValidate, 'with disabled attribute');
+ assert_false(controls[2].i.willValidate, 'with readOnly attribute');
+ assert_false(controls[3].i.willValidate, 'in datalist');
+ assert_false(controls[4].i.willValidate, 'in disabled fieldset');
+}, 'willValidate after upgrade');
+
+test(() => {
+ const element = new NotFormAssociatedElement();
+ assert_throws_dom('NotSupportedError', () => element.i.willValidate);
+}, "willValidate should throw NotSupportedError if the target element is not a form-associated custom element");
+
test(() => {
const control = document.createElement('my-control');
const validity = control.i.validity;