diff --git a/lib/jsdom/living/helpers/form-controls.js b/lib/jsdom/living/helpers/form-controls.js index 07ef2c713a..27d00c40ea 100644 --- a/lib/jsdom/living/helpers/form-controls.js +++ b/lib/jsdom/living/helpers/form-controls.js @@ -24,6 +24,7 @@ const { closest, firstChildWithLocalName } = require("./traversal"); const NODE_TYPE = require("../node-type"); const { HTML_NS } = require("./namespaces"); +// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-fe-disabled exports.isDisabled = formControl => { if (formControl.localName === "button" || formControl.localName === "input" || formControl.localName === "select" || formControl.localName === "textarea") { diff --git a/lib/jsdom/living/nodes/HTMLInputElement-impl.js b/lib/jsdom/living/nodes/HTMLInputElement-impl.js index 21c0bbeb64..d8b0277acd 100644 --- a/lib/jsdom/living/nodes/HTMLInputElement-impl.js +++ b/lib/jsdom/living/nodes/HTMLInputElement-impl.js @@ -57,7 +57,11 @@ const applicableTypesForAttribute = { max: maxMinStepTypes, min: maxMinStepTypes, step: maxMinStepTypes, - pattern: new Set(["text", "search", "tel", "url", "email", "password"]) + pattern: new Set(["text", "search", "tel", "url", "email", "password"]), + readonly: new Set([ + "text", "search", "url", "tel", "email", "password", "date", "month", "week", "time", "datetime-local", + "number" + ]) }; function allowSelect(type) { @@ -182,7 +186,7 @@ class HTMLInputElementImpl extends HTMLElementImpl { } _activationBehavior() { - if (isDisabled(this)) { + if (!this._mutable) { return; } @@ -294,6 +298,10 @@ class HTMLInputElementImpl extends HTMLElementImpl { return this._otherRadioGroupElements.some(radioGroupElement => radioGroupElement.checked); } + get _mutable() { + return !isDisabled(this) && !(this.hasAttributeNS(null, "readonly") && this._attributeApplies("readonly")); + } + get labels() { return getLabelsForLabelable(this); } @@ -857,33 +865,63 @@ class HTMLInputElementImpl extends HTMLElementImpl { // https://html.spec.whatwg.org/multipage/input.html#button-state-(type=button) const willNotValidateTypes = new Set(["hidden", "reset", "button"]); // https://html.spec.whatwg.org/multipage/input.html#attr-input-readonly - const readOnly = this.hasAttributeNS(null, "readonly"); + const readOnly = this.hasAttributeNS(null, "readonly") && this._attributeApplies("readonly"); // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled return willNotValidateTypes.has(this.type) || readOnly; } + // https://html.spec.whatwg.org/multipage/input.html#concept-input-required + get _required() { + return this.hasAttributeNS(null, "required"); + } + get validity() { if (!this._validity) { const state = { // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-being-missing valueMissing: () => { - if (!this.hasAttributeNS(null, "required")) { - return false; + // https://html.spec.whatwg.org/multipage/input.html#the-required-attribute + // Constraint validation: If the element is required, and its value IDL attribute applies + // and is in the mode value, and the element is mutable, and the element's value is the + // empty string, then the element is suffering from being missing. + // + // Note: As of today, the value IDL attribute always applies. + if (this._required && valueAttributeMode(this.type) === "value" && this._mutable && this._value === "") { + return true; } - if (this.type === "checkbox") { + + switch (this.type) { // https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox) // Constraint validation: If the element is required and its checkedness is // false, then the element is suffering from being missing. - return !this.checked; - } else if (this.type === "radio") { + case "checkbox": + if (this._required && !this._checkedness) { + return true; + } + break; + // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio) // Constraint validation: If an element in the radio button group is required, // and all of the input elements in the radio button group have a checkedness // that is false, then the element is suffering from being missing. - return !this._isRadioGroupChecked(); + case "radio": + if (this._required && !this._isRadioGroupChecked()) { + return true; + } + break; + + // https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file) + // Constraint validation: If the element is required and the list of selected files is + // empty, then the element is suffering from being missing. + case "file": + if (this._required && this.files.length === 0) { + return true; + } + break; } - return this.value === ""; + + return false; }, // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-being-too-long diff --git a/lib/jsdom/living/nodes/HTMLSelectElement-impl.js b/lib/jsdom/living/nodes/HTMLSelectElement-impl.js index 38efa265e5..a99537cdbe 100644 --- a/lib/jsdom/living/nodes/HTMLSelectElement-impl.js +++ b/lib/jsdom/living/nodes/HTMLSelectElement-impl.js @@ -12,7 +12,7 @@ const NODE_TYPE = require("../node-type"); const HTMLCollection = require("../generated/HTMLCollection"); const HTMLOptionsCollection = require("../generated/HTMLOptionsCollection"); const { domSymbolTree } = require("../helpers/internal-constants"); -const { getLabelsForLabelable, formOwner } = require("../helpers/form-controls"); +const { getLabelsForLabelable, formOwner, isDisabled } = require("../helpers/form-controls"); class HTMLSelectElementImpl extends HTMLElementImpl { constructor(globalObject, args, privateData) { @@ -120,6 +120,10 @@ class HTMLSelectElementImpl extends HTMLElementImpl { return this.hasAttributeNS(null, "multiple") ? 4 : 1; } + get _mutable() { + return !isDisabled(this); + } + get options() { return this._options; } diff --git a/lib/jsdom/living/nodes/HTMLTextAreaElement-impl.js b/lib/jsdom/living/nodes/HTMLTextAreaElement-impl.js index 7dd33b7725..3170c8935e 100644 --- a/lib/jsdom/living/nodes/HTMLTextAreaElement-impl.js +++ b/lib/jsdom/living/nodes/HTMLTextAreaElement-impl.js @@ -9,7 +9,7 @@ const { mixin } = require("../../utils"); const DOMException = require("domexception/webidl2js-wrapper"); const { cloningSteps } = require("../helpers/internal-constants"); -const { normalizeToCRLF, getLabelsForLabelable, formOwner } = require("../helpers/form-controls"); +const { isDisabled, normalizeToCRLF, getLabelsForLabelable, formOwner } = require("../helpers/form-controls"); const { childTextContent } = require("../helpers/text"); const { fireAnEvent } = require("../helpers/events"); @@ -207,11 +207,15 @@ class HTMLTextAreaElementImpl extends HTMLElementImpl { return this.hasAttributeNS(null, "readonly"); } + get _mutable() { + return !isDisabled(this) && !this.hasAttributeNS(null, "readonly"); + } + // https://html.spec.whatwg.org/multipage/form-elements.html#attr-textarea-required get validity() { if (!this._validity) { const state = { - valueMissing: () => this.hasAttributeNS(null, "required") && this.value === "" + valueMissing: () => this.hasAttributeNS(null, "required") && this._mutable && this.value === "" }; this._validity = ValidityState.createImpl(this._globalObject, [], { diff --git a/test/web-platform-tests/to-run.yaml b/test/web-platform-tests/to-run.yaml index d4fb50cb5b..ec7357cc96 100644 --- a/test/web-platform-tests/to-run.yaml +++ b/test/web-platform-tests/to-run.yaml @@ -557,9 +557,6 @@ formaction.html: [fail, Unknown] DIR: html/semantics/forms/constraints -form-validation-validity-valid.html: [fail, To be fixed] -form-validation-validity-valueMissing.html: [fail, To be fixed] -form-validation-willValidate.html: [fail, To be fixed] infinite_backtracking.html: [timeout, We cannot restrict duration of regex matching from JavaScript] ---