mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-28 07:13:20 +00:00
Bug 1556370
- Part 2: Implement reportValidity() of ElementInternals; r=smaug
Differential Revision: https://phabricator.services.mozilla.com/D130287
This commit is contained in:
parent
16ae3793d4
commit
7ba0e26c7f
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
||||
});
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -1,4 +0,0 @@
|
||||
[ElementInternals-NotSupportedError.html]
|
||||
[Form-related operations and attributes should throw NotSupportedErrors for non-form-associated custom elements.]
|
||||
expected: FAIL
|
||||
|
@ -1,4 +0,0 @@
|
||||
[ElementInternals-validation.html]
|
||||
[reportValidity()]
|
||||
expected: FAIL
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user