From a997ada279592e8aa09fde1af8184b03f790355d Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Mon, 5 Feb 2018 09:57:24 -0800 Subject: [PATCH 01/30] fix(text-field): demo pages on text-field using foundation api to set required --- demos/text-field.html | 47 +++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/demos/text-field.html b/demos/text-field.html index 3a273d74aac..f3e99284e2a 100644 --- a/demos/text-field.html +++ b/demos/text-field.html @@ -139,7 +139,7 @@

Password field with validation

Outlined Text Field

-
@@ -169,6 +169,10 @@

Outlined Text Field

+
+ + +
@@ -382,16 +386,20 @@

Full-Width Text Field and Textarea

tfEl.classList[evt.target.checked ? 'add' : 'remove']('mdc-text-field--dense'); tf.layout(); }); + document.getElementById('outlined-required').addEventListener('change', function(evt) { + var checked = evt.target.checked; + tf.required = checked; + }); }); demoReady(function() { var tfEl = document.getElementById('tf-box-example'); - var tf = new mdc.textField.MDCTextField(tfEl); + var tfBox = new mdc.textField.MDCTextField(tfEl); var wrapper = document.getElementById('demo-tf-box-wrapper'); var helperText = document.getElementById('box-text-field-helper-text'); document.getElementById('box-disable').addEventListener('change', function(evt) { - tf.disabled = evt.target.checked; + tfBox.disabled = evt.target.checked; }); document.getElementById('box-rtl').addEventListener('change', function(evt) { @@ -400,12 +408,12 @@

Full-Width Text Field and Textarea

} else { wrapper.removeAttribute('dir'); } - tf.layout(); + tfBox.layout(); }); document.getElementById('box-dense').addEventListener('change', function(evt) { tfEl.classList[evt.target.checked ? 'add' : 'remove']('mdc-text-field--dense'); - tf.layout(); + tfBox.layout(); }); document.getElementById('box-alternate-colors').addEventListener('change', function (evt) { @@ -419,7 +427,7 @@

Full-Width Text Field and Textarea

var input = tfEl.querySelector('.mdc-text-field__input'); var requiredHelperText = 'Must be at least 8 characters'; var plainHelperText = 'Helper Text (possibly validation message)'; - input.required = target.checked; + tfBox.required = target.checked; input.pattern = evt.target.checked ? '.{8,}' : '.*'; helperText.innerHTML = target.checked ? requiredHelperText : plainHelperText; }); @@ -512,10 +520,14 @@

Full-Width Text Field and Textarea

}); document.getElementById('required-leading-trailing').addEventListener('change', function(evt) { + var checked = evt.target.checked; + tfBoxLeading.required = checked; + tfBoxTrailing.required = checked; + tfOutlinedLeading.required = checked; + tfOutlinedTrailing.required = checked; [].slice.call(tfInputs).forEach(function(input) { - input.required = evt.target.checked; - input.pattern = evt.target.checked ? '.{8,}' : '.*'; - }); + input.pattern = evt.target.checked ? '.{8,}' : '.*'; + }); }); document.getElementById('leading-trailing-alternate-colors').addEventListener('change', function(evt) { @@ -558,7 +570,7 @@

Full-Width Text Field and Textarea

}); document.getElementById('required').addEventListener('change', function(evt) { var target = evt.target; - tfRoot.querySelector('.mdc-text-field__input').required = target.checked; + tf.required = target.checked; }); document.getElementById('alternate-colors').addEventListener('change', function (evt) { var target = evt.target; @@ -608,11 +620,8 @@

Full-Width Text Field and Textarea

}); document.getElementById('textarea-required').addEventListener('change', function(evt) { - var target = evt.target; - [].slice.call(tfRoot.querySelectorAll('.mdc-text-field__input')) - .forEach(function(input) { - input.required = target.checked; - }) + var checked = evt.target.checked; + tf.required = checked; }); }); @@ -638,11 +647,9 @@

Full-Width Text Field and Textarea

}); document.getElementById('fullwidth-required').addEventListener('change', function(evt) { - var target = evt.target; - [].slice.call(section.querySelectorAll('.mdc-text-field__input')) - .forEach(function(input) { - input.required = target.checked; - }) + var checked = evt.target.checked; + tf.required = checked; + tfMulti.required = checked; }); document.getElementById('fullwidth-alternate-colors').addEventListener('change', function (evt) { From 2a92713edf91db224c84323fc813ec1938bd735b Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Mon, 5 Feb 2018 10:52:42 -0800 Subject: [PATCH 02/30] fix(text-field): removed check validity from setRequired in foundation --- packages/mdc-textfield/foundation.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/mdc-textfield/foundation.js b/packages/mdc-textfield/foundation.js index fb76510e23d..25bc6d7123d 100644 --- a/packages/mdc-textfield/foundation.js +++ b/packages/mdc-textfield/foundation.js @@ -301,9 +301,6 @@ class MDCTextFieldFoundation extends MDCFoundation { */ setRequired(isRequired) { this.getNativeInput_().required = isRequired; - // Addition of the asterisk is automatic based on CSS, but validity checking - // needs to be manually run. - this.styleValidity_(this.isValid()); } /** From 3bc98ee60b0a34669646763d89ebd036ccd2d50c Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Mon, 5 Feb 2018 11:40:42 -0800 Subject: [PATCH 03/30] fix(text-field): fixed foundation test #setRequired --- test/unit/mdc-textfield/foundation.test.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/unit/mdc-textfield/foundation.test.js b/test/unit/mdc-textfield/foundation.test.js index 33cee653ced..78ec1c03681 100644 --- a/test/unit/mdc-textfield/foundation.test.js +++ b/test/unit/mdc-textfield/foundation.test.js @@ -204,19 +204,15 @@ test('#setValid updates classes', () => { test('#setRequired updates CSS classes', () => { // Native validity checking does not apply in unittests, so manually mark as valid or invalid. - const {foundation, mockAdapter, nativeInput, helperText} = + const {foundation, mockAdapter, nativeInput} = setupValueTest('', /* isValid */ false); foundation.setRequired(true); assert.isOk(foundation.isRequired()); - td.verify(mockAdapter.addClass(cssClasses.INVALID)); - td.verify(helperText.setValidity(false)); nativeInput.validity.valid = true; foundation.setRequired(false); assert.isNotOk(foundation.isRequired()); - td.verify(mockAdapter.removeClass(cssClasses.INVALID)); - td.verify(helperText.setValidity(true)); // None of these is affected by setRequired. td.verify(mockAdapter.addClass(cssClasses.FOCUSED), {times: 0}); From 09932f8ff0d7557dfc7d2757fcd6e59c5a0b25aa Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Mon, 5 Feb 2018 11:50:02 -0800 Subject: [PATCH 04/30] fix(text-field): undo demo var rename --- demos/text-field.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/demos/text-field.html b/demos/text-field.html index f3e99284e2a..0f6ca3a2f7f 100644 --- a/demos/text-field.html +++ b/demos/text-field.html @@ -394,12 +394,12 @@

Full-Width Text Field and Textarea

demoReady(function() { var tfEl = document.getElementById('tf-box-example'); - var tfBox = new mdc.textField.MDCTextField(tfEl); + var tf = new mdc.textField.MDCTextField(tfEl); var wrapper = document.getElementById('demo-tf-box-wrapper'); var helperText = document.getElementById('box-text-field-helper-text'); document.getElementById('box-disable').addEventListener('change', function(evt) { - tfBox.disabled = evt.target.checked; + tf.disabled = evt.target.checked; }); document.getElementById('box-rtl').addEventListener('change', function(evt) { @@ -408,12 +408,12 @@

Full-Width Text Field and Textarea

} else { wrapper.removeAttribute('dir'); } - tfBox.layout(); + tf.layout(); }); document.getElementById('box-dense').addEventListener('change', function(evt) { tfEl.classList[evt.target.checked ? 'add' : 'remove']('mdc-text-field--dense'); - tfBox.layout(); + tf.layout(); }); document.getElementById('box-alternate-colors').addEventListener('change', function (evt) { @@ -427,7 +427,7 @@

Full-Width Text Field and Textarea

var input = tfEl.querySelector('.mdc-text-field__input'); var requiredHelperText = 'Must be at least 8 characters'; var plainHelperText = 'Helper Text (possibly validation message)'; - tfBox.required = target.checked; + tf.required = target.checked; input.pattern = evt.target.checked ? '.{8,}' : '.*'; helperText.innerHTML = target.checked ? requiredHelperText : plainHelperText; }); From 10acedddfd33fb15ed64e6a55b4b101ae6182894 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Fri, 9 Feb 2018 16:08:59 -0800 Subject: [PATCH 05/30] fix(text-field): added set/get/remove validationAttribute BREAKING CHANGE: removed setRequired and isRequired from foundation. Removed get/set required from component. --- demos/text-field.html | 112 ++++++++++++++---- packages/mdc-textfield/foundation.js | 27 ++++- packages/mdc-textfield/index.js | 21 +++- test/unit/mdc-textfield/foundation.test.js | 38 ++++-- .../unit/mdc-textfield/mdc-text-field.test.js | 20 +++- 5 files changed, 169 insertions(+), 49 deletions(-) diff --git a/demos/text-field.html b/demos/text-field.html index 0f6ca3a2f7f..4e29b98c140 100644 --- a/demos/text-field.html +++ b/demos/text-field.html @@ -139,7 +139,7 @@

Password field with validation

Outlined Text Field

-
@@ -171,7 +171,11 @@

Outlined Text Field

- + +
+
+ +
@@ -207,7 +211,11 @@

Text Field box

- + +
+
+ +
@@ -289,7 +297,11 @@

Text Field - Leading/Trailing icons

- + +
+
+ +
@@ -388,7 +400,19 @@

Full-Width Text Field and Textarea

}); document.getElementById('outlined-required').addEventListener('change', function(evt) { var checked = evt.target.checked; - tf.required = checked; + if (checked) { + tf.setValidationAttribute('required', 'true'); + } else { + tf.removeValidationAttribute('required'); + } + }); + document.getElementById('outlined-pattern').addEventListener('change', function(evt) { + var checked = evt.target.checked; + if (checked) { + tf.setValidationAttribute('minlength', 8); + } else { + tf.removeValidationAttribute('minlength'); + } }); }); @@ -423,13 +447,23 @@

Full-Width Text Field and Textarea

}); document.getElementById('box-required').addEventListener('change', function(evt) { - var target = evt.target; - var input = tfEl.querySelector('.mdc-text-field__input'); + var checked = evt.target.checked; + if (checked) { + tf.setValidationAttribute('required', 'true'); + } else { + tf.removeValidationAttribute('required'); + } + }); + document.getElementById('box-pattern').addEventListener('change', function(evt) { + var checked = evt.target.checked; var requiredHelperText = 'Must be at least 8 characters'; var plainHelperText = 'Helper Text (possibly validation message)'; - tf.required = target.checked; - input.pattern = evt.target.checked ? '.{8,}' : '.*'; - helperText.innerHTML = target.checked ? requiredHelperText : plainHelperText; + if (checked) { + tf.setValidationAttribute('pattern', '.{8,}'); + } else { + tf.setValidationAttribute('pattern', '.*'); + } + helperText.innerHTML = checked ? requiredHelperText : plainHelperText; }); document.getElementById('box-use-helper-text').addEventListener('change', function(evt) { @@ -521,13 +555,32 @@

Full-Width Text Field and Textarea

document.getElementById('required-leading-trailing').addEventListener('change', function(evt) { var checked = evt.target.checked; - tfBoxLeading.required = checked; - tfBoxTrailing.required = checked; - tfOutlinedLeading.required = checked; - tfOutlinedTrailing.required = checked; - [].slice.call(tfInputs).forEach(function(input) { - input.pattern = evt.target.checked ? '.{8,}' : '.*'; - }); + if (checked) { + tfBoxLeading.setValidationAttribute('required', 'true'); + tfBoxTrailing.setValidationAttribute('required', 'true'); + tfOutlinedLeading.setValidationAttribute('required', 'true'); + tfOutlinedTrailing.setValidationAttribute('required', 'true'); + } else { + tfBoxLeading.removeValidationAttribute('required'); + tfBoxTrailing.removeValidationAttribute('required'); + tfOutlinedLeading.removeValidationAttribute('required'); + tfOutlinedTrailing.removeValidationAttribute('required'); + } + }); + + document.getElementById('pattern-leading-trailing').addEventListener('change', function(evt) { + var checked = evt.target.checked; + if (checked) { + tfBoxLeading.setValidationAttribute('pattern', '.{8,}'); + tfBoxTrailing.setValidationAttribute('pattern', '.{8,}'); + tfOutlinedLeading.setValidationAttribute('pattern', '.{8,}'); + tfOutlinedTrailing.setValidationAttribute('pattern', '.{8,}'); + } else { + tfBoxLeading.setValidationAttribute('pattern', '.*'); + tfBoxTrailing.setValidationAttribute('pattern', '.*'); + tfOutlinedLeading.setValidationAttribute('pattern', '.*'); + tfOutlinedTrailing.setValidationAttribute('pattern', '.*'); + } }); document.getElementById('leading-trailing-alternate-colors').addEventListener('change', function(evt) { @@ -569,8 +622,12 @@

Full-Width Text Field and Textarea

tfRoot.classList[target.checked ? 'add' : 'remove']('mdc-text-field--dense'); }); document.getElementById('required').addEventListener('change', function(evt) { - var target = evt.target; - tf.required = target.checked; + var checked = evt.target.checked; + if (checked) { + tf.setValidationAttribute('required', 'true'); + } else { + tf.removeValidationAttribute('required'); + } }); document.getElementById('alternate-colors').addEventListener('change', function (evt) { var target = evt.target; @@ -618,10 +675,13 @@

Full-Width Text Field and Textarea

var target = evt.target; tfRoot.classList[target.checked ? 'add' : 'remove']('demo-textarea'); }); - document.getElementById('textarea-required').addEventListener('change', function(evt) { var checked = evt.target.checked; - tf.required = checked; + if (checked) { + tf.setValidationAttribute('required', 'true'); + } else { + tf.removeValidationAttribute('required'); + } }); }); @@ -645,11 +705,15 @@

Full-Width Text Field and Textarea

el.classList[target.checked ? 'add' : 'remove']('mdc-text-field--dense'); }); }); - document.getElementById('fullwidth-required').addEventListener('change', function(evt) { var checked = evt.target.checked; - tf.required = checked; - tfMulti.required = checked; + if (checked) { + tf.setValidationAttribute('required', 'true'); + tfMulti.setValidationAttribute('required', 'true'); + } else { + tf.removeValidationAttribute('required'); + tfMulti.removeValidationAttribute('required'); + } }); document.getElementById('fullwidth-alternate-colors').addEventListener('change', function (evt) { diff --git a/packages/mdc-textfield/foundation.js b/packages/mdc-textfield/foundation.js index 25bc6d7123d..5077774d439 100644 --- a/packages/mdc-textfield/foundation.js +++ b/packages/mdc-textfield/foundation.js @@ -290,17 +290,32 @@ class MDCTextFieldFoundation extends MDCFoundation { } /** - * @return {boolean} True if the Text Field is required. + * @return {string} the attribute value from the input element + * @param {string} attrName is the name of the attribute on the input element */ - isRequired() { - return this.getNativeInput_().required; + getValidationAttribute(attrName) { + return this.getNativeInput_().getAttribute(attrName); } /** - * @param {boolean} isRequired Sets the text-field required or not. + * @param {string} attrName is the name of the attribute to be set + * @param {boolean|number|string} val is the value of attribute to be set */ - setRequired(isRequired) { - this.getNativeInput_().required = isRequired; + setValidationAttribute(attrName, val) { + this.getNativeInput_().setAttribute(attrName, val); + // Setting validation to true even if input is incorrect. + // If developer toggles a validation property (required, minlength, etc) + // the user should have a chance interact with the input before it being + // invalid again. + this.styleValidity_(true); + } + + /** + * @param {string} attrName is the name of the attribute to remove from input element + */ + removeValidationAttribute(attrName) { + this.getNativeInput_().removeAttribute(attrName); + this.styleValidity_(true); } /** diff --git a/packages/mdc-textfield/index.js b/packages/mdc-textfield/index.js index ffd841e9861..784dc5d8a95 100644 --- a/packages/mdc-textfield/index.js +++ b/packages/mdc-textfield/index.js @@ -203,17 +203,26 @@ class MDCTextField extends MDCComponent { } /** - * @return {boolean} True if the Text Field is required. + * @return {string} the attribute value from the input element + * @param {string} attrName is the name of the attribute on the input element */ - get required() { - return this.foundation_.isRequired(); + getValidationAttribute(attrName) { + return this.foundation_.getValidationAttribute(attrName); } /** - * @param {boolean} required Sets the Text Field to required. + * @param {string} attrName is the attribute name to be set + * @param {boolean|number|string} val is the value of attribute to be set */ - set required(required) { - this.foundation_.setRequired(required); + setValidationAttribute(attrName, val) { + this.foundation_.setValidationAttribute(attrName, val); + } + + /** + * @param {string} attrName is the name of the attribute to remove from input element + */ + removeValidationAttribute(attrName) { + this.foundation_.removeValidationAttribute(attrName); } /** diff --git a/test/unit/mdc-textfield/foundation.test.js b/test/unit/mdc-textfield/foundation.test.js index 78ec1c03681..a54c04748a4 100644 --- a/test/unit/mdc-textfield/foundation.test.js +++ b/test/unit/mdc-textfield/foundation.test.js @@ -202,25 +202,47 @@ test('#setValid updates classes', () => { td.verify(mockAdapter.removeClass(cssClasses.DISABLED), {times: 0}); }); -test('#setRequired updates CSS classes', () => { +test('#setValidationAttribute required to true updates CSS classes', () => { // Native validity checking does not apply in unittests, so manually mark as valid or invalid. - const {foundation, mockAdapter, nativeInput} = + const {foundation, mockAdapter, helperText} = setupValueTest('', /* isValid */ false); - foundation.setRequired(true); - assert.isOk(foundation.isRequired()); + td.when(mockAdapter.getNativeInput()).thenReturn({setAttribute: () => null}); + foundation.setValidationAttribute('required', 'true'); + td.verify(mockAdapter.removeClass(cssClasses.INVALID)); + td.verify(helperText.setValidity(true)); - nativeInput.validity.valid = true; - foundation.setRequired(false); - assert.isNotOk(foundation.isRequired()); + // None of these is affected by setValidationAttribute. + td.verify(mockAdapter.addClass(cssClasses.FOCUSED), {times: 0}); + td.verify(mockAdapter.removeClass(cssClasses.FOCUSED), {times: 0}); + td.verify(mockAdapter.addClass(cssClasses.DISABLED), {times: 0}); + td.verify(mockAdapter.removeClass(cssClasses.DISABLED), {times: 0}); +}); - // None of these is affected by setRequired. +test('#removeValidationAttribute required updates CSS classes', () => { + const {foundation, mockAdapter, helperText} = + setupValueTest('', /* isValid */ false); + + td.when(mockAdapter.getNativeInput()).thenReturn({removeAttribute: () => null}); + foundation.removeValidationAttribute('required'); + td.verify(mockAdapter.removeClass(cssClasses.INVALID)); + td.verify(helperText.setValidity(true)); + + // None of these is affected by setValidationAttribute. td.verify(mockAdapter.addClass(cssClasses.FOCUSED), {times: 0}); td.verify(mockAdapter.removeClass(cssClasses.FOCUSED), {times: 0}); td.verify(mockAdapter.addClass(cssClasses.DISABLED), {times: 0}); td.verify(mockAdapter.removeClass(cssClasses.DISABLED), {times: 0}); }); +test('#getValidationAttribute required returns correct value', () => { + const {foundation, mockAdapter} = + setupValueTest('', /* isValid */ false); + + td.when(mockAdapter.getNativeInput()).thenReturn({getAttribute: () => true}); + assert.equal(foundation.getValidationAttribute('required'), true); +}); + test('#setDisabled flips disabled when a native input is given', () => { const {foundation, mockAdapter} = setupTest(); const nativeInput = {disabled: false}; diff --git a/test/unit/mdc-textfield/mdc-text-field.test.js b/test/unit/mdc-textfield/mdc-text-field.test.js index c19fb5e5102..b339e94eb97 100644 --- a/test/unit/mdc-textfield/mdc-text-field.test.js +++ b/test/unit/mdc-textfield/mdc-text-field.test.js @@ -416,10 +416,20 @@ test('get/set valid', () => { td.verify(mockFoundation.setValid(true)); }); -test('get/set required', () => { +test('setValidationAttribute', () => { const {component, mockFoundation} = setupMockFoundationTest(); - component.required; - td.verify(mockFoundation.isRequired()); - component.required = true; - td.verify(mockFoundation.setRequired(true)); + component.setValidationAttribute('required', true); + td.verify(mockFoundation.setValidationAttribute('required', true)); +}); + +test('removeValidationAttribute', () => { + const {component, mockFoundation} = setupMockFoundationTest(); + component.removeValidationAttribute('required'); + td.verify(mockFoundation.removeValidationAttribute('required')); +}); + +test('getValidationAttribute', () => { + const {component, mockFoundation} = setupMockFoundationTest(); + component.getValidationAttribute('required'); + td.verify(mockFoundation.getValidationAttribute('required')); }); From 411f0bff2655e466e1a12f3823e3d1d4a30d23ef Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Fri, 9 Feb 2018 16:34:23 -0800 Subject: [PATCH 06/30] fix(text-field): update demo page code --- demos/text-field.html | 68 +++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/demos/text-field.html b/demos/text-field.html index 4e29b98c140..44ae2aff4cc 100644 --- a/demos/text-field.html +++ b/demos/text-field.html @@ -149,8 +149,8 @@

Outlined Text Field

-

- Must be at least 8 characters +

+ Helper Text (possibly validation message)

@@ -375,8 +375,16 @@

Full-Width Text Field and Textarea