Bug 1556370 - Part 2: Implement reportValidity() of ElementInternals; r=smaug

Differential Revision: https://phabricator.services.mozilla.com/D130287
This commit is contained in:
Edgar Chen 2021-12-09 18:06:04 +00:00
parent 16ae3793d4
commit 7ba0e26c7f
13 changed files with 234 additions and 14 deletions

View File

@ -30,7 +30,7 @@ class FormValidationChild extends JSWindowActorChild {
switch (aEvent.type) {
case "MozInvalidForm":
aEvent.preventDefault();
this.notifyInvalidSubmit(aEvent.target, aEvent.detail);
this.notifyInvalidSubmit(aEvent.detail);
break;
case "pageshow":
if (this._isRootDocumentEvent(aEvent)) {
@ -51,7 +51,7 @@ class FormValidationChild extends JSWindowActorChild {
}
}
notifyInvalidSubmit(aFormElement, aInvalidElements) {
notifyInvalidSubmit(aInvalidElements) {
// Show a validation message on the first focusable element.
for (let element of aInvalidElements) {
// Insure that this is the FormSubmitObserver associated with the
@ -65,18 +65,29 @@ class FormValidationChild extends JSWindowActorChild {
ChromeUtils.getClassName(element) === "HTMLInputElement" ||
ChromeUtils.getClassName(element) === "HTMLTextAreaElement" ||
ChromeUtils.getClassName(element) === "HTMLSelectElement" ||
ChromeUtils.getClassName(element) === "HTMLButtonElement"
ChromeUtils.getClassName(element) === "HTMLButtonElement" ||
element.isFormAssociatedCustomElements
)
) {
continue;
}
if (!Services.focus.elementIsFocusable(element, 0)) {
let validationMessage = element.isFormAssociatedCustomElements
? element.internals.validationMessage
: element.validationMessage;
if (element.isFormAssociatedCustomElements) {
// For element that are form-associated custom elements, user agents
// should use their validation anchor instead.
element = element.internals.validationAnchor;
}
if (!element || !Services.focus.elementIsFocusable(element, 0)) {
continue;
}
// Update validation message before showing notification
this._validationMessage = element.validationMessage;
this._validationMessage = validationMessage;
// Don't connect up to the same element more than once.
if (this._element == element) {

View File

@ -7,6 +7,7 @@
#include "mozilla/dom/ElementInternals.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/CustomEvent.h"
#include "mozilla/dom/ElementInternalsBinding.h"
#include "mozilla/dom/FormData.h"
#include "mozilla/dom/HTMLElement.h"
@ -242,6 +243,49 @@ bool ElementInternals::CheckValidity(ErrorResult& aRv) {
return nsIConstraintValidation::CheckValidity(*mTarget);
}
// https://html.spec.whatwg.org/#dom-elementinternals-reportvalidity
bool ElementInternals::ReportValidity(ErrorResult& aRv) {
if (!mTarget || !mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return false;
}
bool defaultAction = true;
if (nsIConstraintValidation::CheckValidity(*mTarget, &defaultAction)) {
return true;
}
if (!defaultAction) {
return false;
}
AutoTArray<RefPtr<Element>, 1> invalidElements;
invalidElements.AppendElement(mTarget);
AutoJSAPI jsapi;
if (!jsapi.Init(mTarget->GetOwnerGlobal())) {
return false;
}
JS::Rooted<JS::Value> detail(jsapi.cx());
if (!ToJSValue(jsapi.cx(), invalidElements, &detail)) {
return false;
}
mTarget->UpdateState(true);
RefPtr<CustomEvent> event =
NS_NewDOMCustomEvent(mTarget->OwnerDoc(), nullptr, nullptr);
event->InitCustomEvent(jsapi.cx(), u"MozInvalidForm"_ns,
/* CanBubble */ true,
/* Cancelable */ true, detail);
event->SetTrusted(true);
event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
mTarget->DispatchEvent(*event);
return false;
}
// https://html.spec.whatwg.org/#dom-elementinternals-labels
already_AddRefed<nsINodeList> ElementInternals::GetLabels(
ErrorResult& aRv) const {
@ -253,6 +297,16 @@ already_AddRefed<nsINodeList> ElementInternals::GetLabels(
return mTarget->Labels();
}
nsGenericHTMLElement* ElementInternals::GetValidationAnchor(
ErrorResult& aRv) const {
if (!mTarget || !mTarget->IsFormAssociatedElement()) {
aRv.ThrowNotSupportedError(
"Target element is not a form-associated custom element");
return nullptr;
}
return mValidationAnchor;
}
void ElementInternals::SetForm(HTMLFormElement* aForm) { mForm = aForm; }
void ElementInternals::ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) {

View File

@ -59,7 +59,9 @@ class ElementInternals final : public nsIFormControl,
void GetValidationMessage(nsAString& aValidationMessage,
ErrorResult& aRv) const;
bool CheckValidity(ErrorResult& aRv);
bool ReportValidity(ErrorResult& aRv);
already_AddRefed<nsINodeList> GetLabels(ErrorResult& aRv) const;
nsGenericHTMLElement* GetValidationAnchor(ErrorResult& aRv) const;
// nsIFormControl
mozilla::dom::HTMLFieldSetElement* GetFieldSet() override {

View File

@ -2980,6 +2980,20 @@ already_AddRefed<ElementInternals> nsGenericHTMLElement::AttachInternals(
return nullptr;
}
ElementInternals* nsGenericHTMLElement::GetInternals() const {
if (CustomElementData* data = GetCustomElementData()) {
return data->GetElementInternals();
}
return nullptr;
}
bool nsGenericHTMLElement::IsFormAssociatedCustomElements() const {
if (CustomElementData* data = GetCustomElementData()) {
return data->IsFormAssociated();
}
return false;
}
void nsGenericHTMLElement::GetAutocapitalize(nsAString& aValue) const {
GetEnumAttr(nsGkAtoms::autocapitalize, nullptr, kDefaultAutocapitalize->tag,
aValue);

View File

@ -262,6 +262,10 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase {
virtual already_AddRefed<mozilla::dom::ElementInternals> AttachInternals(
ErrorResult& aRv);
mozilla::dom::ElementInternals* GetInternals() const;
bool IsFormAssociatedCustomElements() const;
// Returns true if the event should not be handled from GetEventTargetParent.
virtual bool IsDisabledForEvents(mozilla::WidgetEvent* aEvent) {
return false;

View File

@ -57,6 +57,9 @@ skip-if = e10s
[browser_data_document_crossOriginIsolated.js]
[browser_focus_steal_from_chrome.js]
[browser_focus_steal_from_chrome_during_mousedown.js]
[browser_form_associated_custom_elements_validity.js]
support-files =
file_empty.html
[browser_frame_elements.js]
[browser_hasbeforeunload.js]
https_first_disabled = true

View File

@ -0,0 +1,119 @@
/**
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
add_task(async function init() {
await SpecialPowers.pushPrefEnv({
set: [["dom.webcomponents.formAssociatedCustomElement.enabled", true]],
});
});
add_task(async function report_validity() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: `data:text/html,<my-control></my-control>`,
},
async function(aBrowser) {
let promisePopupShown = BrowserTestUtils.waitForEvent(
window,
"popupshown"
);
let message = "valueMissing message";
await SpecialPowers.spawn(aBrowser, [message], function(aMessage) {
class MyControl extends content.HTMLElement {
static get formAssociated() {
return true;
}
constructor() {
super();
let shadow = this.attachShadow({ mode: "open" });
let input = content.document.createElement("input");
shadow.appendChild(input);
let internals = this.attachInternals();
internals.setValidity({ valueMissing: true }, aMessage, input);
internals.reportValidity();
}
}
content.customElements.define("my-control", MyControl);
let myControl = content.document.querySelector("my-control");
content.customElements.upgrade(myControl);
});
await promisePopupShown;
let invalidFormPopup = window.document.getElementById(
"invalid-form-popup"
);
is(invalidFormPopup.state, "open", "invalid-form-popup should be opened");
is(invalidFormPopup.firstChild.textContent, message, "check message");
let promisePopupHidden = BrowserTestUtils.waitForEvent(
invalidFormPopup,
"popuphidden"
);
invalidFormPopup.hidePopup();
await promisePopupHidden;
}
);
});
add_task(async function form_report_validity() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: `data:text/html,<form><my-control></my-control></form>`,
},
async function(aBrowser) {
let promisePopupShown = BrowserTestUtils.waitForEvent(
window,
"popupshown"
);
let message = "valueMissing message";
await SpecialPowers.spawn(aBrowser, [message], function(aMessage) {
class MyControl extends content.HTMLElement {
static get formAssociated() {
return true;
}
constructor() {
super();
let shadow = this.attachShadow({ mode: "open" });
let input = content.document.createElement("input");
shadow.appendChild(input);
let internals = this.attachInternals();
internals.setValidity({ valueMissing: true }, aMessage, input);
}
}
content.customElements.define("my-control", MyControl);
let myControl = content.document.querySelector("my-control");
content.customElements.upgrade(myControl);
let form = content.document.querySelector("form");
is(form.length, "1", "check form.length");
form.reportValidity();
});
await promisePopupShown;
let invalidFormPopup = window.document.getElementById(
"invalid-form-popup"
);
is(invalidFormPopup.state, "open", "invalid-form-popup should be opened");
is(invalidFormPopup.firstChild.textContent, message, "check message");
let promisePopupHidden = BrowserTestUtils.waitForEvent(
invalidFormPopup,
"popuphidden"
);
invalidFormPopup.hidePopup();
await promisePopupHidden;
}
);
});

View File

@ -32,11 +32,18 @@ interface ElementInternals {
readonly attribute DOMString validationMessage;
[Pref="dom.webcomponents.formAssociatedCustomElement.enabled", Throws]
boolean checkValidity();
[Pref="dom.webcomponents.formAssociatedCustomElement.enabled", Throws]
boolean reportValidity();
[Pref="dom.webcomponents.formAssociatedCustomElement.enabled", Throws]
readonly attribute NodeList labels;
};
partial interface ElementInternals {
[ChromeOnly, Throws]
readonly attribute HTMLElement? validationAnchor;
};
dictionary ValidityStateFlags {
boolean valueMissing = false;
boolean typeMismatch = false;

View File

@ -82,6 +82,14 @@ partial interface HTMLElement {
readonly attribute long offsetHeight;
};
partial interface HTMLElement {
[ChromeOnly]
readonly attribute ElementInternals? internals;
[ChromeOnly]
readonly attribute boolean isFormAssociatedCustomElements;
};
interface mixin TouchEventHandlers {
[Func="nsGenericHTMLElement::LegacyTouchAPIEnabled"]
attribute EventHandler ontouchstart;

View File

@ -1,4 +0,0 @@
[ElementInternals-NotSupportedError.html]
[Form-related operations and attributes should throw NotSupportedErrors for non-form-associated custom elements.]
expected: FAIL

View File

@ -1,4 +0,0 @@
[ElementInternals-validation.html]
[reportValidity()]
expected: FAIL

View File

@ -93,7 +93,8 @@ prefs: [dom.security.featurePolicy.experimental.enabled:true, dom.security.featu
expected: FAIL
[ElementInternals interface: operation reportValidity()]
expected: FAIL
expected:
if release_or_beta: FAIL
[VideoTrackList interface object length]
expected: FAIL

View File

@ -248,6 +248,11 @@ test(() => {
assert_equals(invalidCount, 1);
}, 'checkValidity()');
test(() => {
const element = new NotFormAssociatedElement();
assert_throws_dom('NotSupportedError', () => element.i.reportValidity());
}, "reportValidity() should throw NotSupportedError if the target element is not a form-associated custom element");
test(() => {
const control = document.createElement('my-control');
document.body.appendChild(control);