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;