]> git.ipfire.org Git - thirdparty/foundation/foundation-sites.git/commitdiff
feat: add required checkbox support to Abide
authorSassNinja <kai.falkowski@gmail.com>
Mon, 11 Mar 2019 12:52:48 +0000 (13:52 +0100)
committerSassNinja <kai.falkowski@gmail.com>
Mon, 11 Mar 2019 12:52:48 +0000 (13:52 +0100)
This fixes #11683

js/foundation.abide.js
test/javascript/components/abide.js
test/visual/abide/abide-checkboxes.html [new file with mode: 0644]

index 4a6741301147350526ee718b76ab9ffbaf350e16..7fb0c294a02f7bc3a460dc03f0e25aab23fd17d8 100644 (file)
@@ -134,7 +134,7 @@ class Abide extends Plugin {
    * @returns {Object} jQuery object with the selector.
    */
   findFormError($el) {
-    var id = $el[0].id;
+    var id = $el.length ? $el[0].id : '';
     var $error = $el.siblings(this.options.formErrorSelector);
 
     if (!$error.length) {
@@ -189,6 +189,28 @@ class Abide extends Plugin {
     return $(labels);
   }
 
+  /**
+   * Get the set of labels associated with a set of checkbox els in this order
+   * 2. The <label> with the attribute `[for="someInputId"]`
+   * 3. The `.closest()` <label>
+   *
+   * @param {Object} $el - jQuery object to check for required attribute
+   * @returns {Boolean} Boolean value depends on whether or not attribute is checked or empty
+   */
+  findCheckboxLabels($els) {
+    var labels = $els.map((i, el) => {
+      var id = el.id;
+      var $label = this.$element.find(`label[for="${id}"]`);
+
+      if (!$label.length) {
+        $label = $(el).closest('label');
+      }
+      return $label[0];
+    });
+
+    return $(labels);
+  }
+
   /**
    * Adds the CSS error class as specified by the Abide settings to the label, input, and the form
    * @param {Object} $el - jQuery object to add the class to
@@ -292,6 +314,31 @@ class Abide extends Plugin {
 
   }
 
+  /**
+   * Remove CSS error classes etc from an entire checkbox group
+   * @param {String} groupName - A string that specifies the name of a checkbox group
+   *
+   */
+  removeCheckboxErrorClasses(groupName) {
+    var $els = this.$element.find(`:checkbox[name="${groupName}"]`);
+    var $labels = this.findCheckboxLabels($els);
+    var $formErrors = this.findFormError($els);
+
+    if ($labels.length) {
+      $labels.removeClass(this.options.labelErrorClass);
+    }
+
+    if ($formErrors.length) {
+      $formErrors.removeClass(this.options.formErrorClass);
+    }
+
+    $els.removeClass(this.options.inputErrorClass).attr({
+      'data-invalid': null,
+      'aria-invalid': null
+    });
+
+  }
+
   /**
    * Removes CSS error class as specified by the Abide settings from the label, input, and the form
    * @param {Object} $el - jQuery object to remove the class from
@@ -301,6 +348,10 @@ class Abide extends Plugin {
     if($el[0].type == 'radio') {
       return this.removeRadioErrorClasses($el.attr('name'));
     }
+    // checkboxes need to clear all of the els
+    else if($el[0].type == 'checkbox') {
+      return this.removeCheckboxErrorClasses($el.attr('name'));
+    }
 
     var $label = this.findLabel($el);
     var $formError = this.findFormError($el);
@@ -345,7 +396,8 @@ class Abide extends Plugin {
         break;
 
       case 'checkbox':
-        validated = clearRequire;
+        validated = this.validateCheckbox($el.attr('name'));
+        clearRequire = true;
         break;
 
       case 'select':
@@ -405,8 +457,21 @@ class Abide extends Plugin {
   validateForm() {
     var acc = [];
     var _this = this;
+    var checkboxGroupName;
+
+    // Remember first form submission to prevent specific checkbox validation (more than one required) until form got initially submitted
+    if (!this.initialized) {
+      this.initialized = true;
+    }
 
     this.$inputs.each(function() {
+
+      // Only use one checkbox per group since validateCheckbox() iterates over all associated checkboxes
+      if ($(this)[0].type === 'checkbox') {
+        if ($(this).attr('name') === checkboxGroupName) return true;
+        checkboxGroupName = $(this).attr('name');
+      }
+
       acc.push(_this.validateInput($(this)));
     });
 
@@ -495,6 +560,60 @@ class Abide extends Plugin {
     return valid;
   }
 
+  /**
+   * Determines whether or a not a checkbox input is valid based on whether or not it is required and checked. Although the function targets a single `<input>`, it validates by checking the `required` and `checked` properties of all checkboxes in its group.
+   * @param {String} groupName - A string that specifies the name of a checkbox group
+   * @returns {Boolean} Boolean value depends on whether or not at least one checkbox input has been checked (if it's required)
+   */
+  validateCheckbox(groupName) {
+    // If at least one checkbox in the group has the `required` attribute, the group is considered required
+    // Per W3C spec, all checkboxes in a group should have `required`, but we're being nice
+    var $group = this.$element.find(`:checkbox[name="${groupName}"]`);
+    var valid = false, required = false, minRequired = 1, checked = 0;
+
+    // For the group to be required, at least one checkbox needs to be required
+    $group.each((i, e) => {
+      if ($(e).attr('required')) {
+        required = true;
+      }
+    });
+    if(!required) valid=true;
+
+    if (!valid) {
+      // Count checked checkboxes within the group
+      // Use data-min-required if available (default: 1)
+      $group.each((i, e) => {
+        if ($(e).prop('checked')) {
+          checked++;
+        }
+        if (typeof $(e).attr('data-min-required') !== 'undefined') {
+          minRequired = parseInt($(e).attr('data-min-required'));
+        }
+      });
+
+      // For the group to be valid, the minRequired amount of checkboxes have to be checked
+      if (checked >= minRequired) {
+        valid = true;
+      }
+    };
+
+    // Skip validation if more than 1 checkbox have to be checked AND if the form hasn't got submitted yet (otherwise it will already show an error during the first fill in)
+    if (this.initialized !== true && minRequired > 1) {
+      return true;
+    }
+
+    // Refresh error class for all input
+    $group.each((i, e) => {
+      if (!valid) {
+        this.addErrorClasses($(e));
+      } else {
+        this.removeErrorClasses($(e));
+      }
+    });
+
+    return valid;
+  }
+
   /**
    * Determines if a selected input passes a custom validation function. Multiple validations can be used, if passed to the element with `data-validator="foo bar baz"` in a space separated listed.
    * @param {Object} $el - jQuery input element.
index bca4420482f6df0c5c1475ba9a0a91e15acc4002..e49286d63bc1c0ffe6f31d21405e4e862843e270 100644 (file)
@@ -52,14 +52,14 @@ describe('Abide', function() {
     });
 
     it('returns true for checked checkboxes', function() {
-      $html = $("<form data-abide><input type='checkbox' required checked='checked'></form>").appendTo("body");
+      $html = $("<form data-abide novalidate><input type='checkbox' name='groupName' required checked='checked'></form>").appendTo("body");
       plugin = new Foundation.Abide($html, {});
 
       plugin.validateInput($html.find("input")).should.equal(true);
     });
 
     it('returns false for unchecked checkboxes', function() {
-      $html = $("<form data-abide><input type='checkbox' required></form>").appendTo("body");
+      $html = $("<form data-abide novalidate><input type='checkbox' name='groupName' required></form>").appendTo("body");
       plugin = new Foundation.Abide($html, {});
 
       plugin.validateInput($html.find("input")).should.equal(false);
@@ -163,6 +163,19 @@ describe('Abide', function() {
     });
   });
 
+  describe('removeCheckboxErrorClasses()', function() {
+    it('removes aria-invalid attribute from checkbox group', function() {
+      $html = $('<form data-abide><input type="checkbox" name="groupName"></form>').appendTo('body');
+      plugin = new Foundation.Abide($html, {});
+      // Add error classes first
+      plugin.addErrorClasses($html.find('input'));
+
+      plugin.removeCheckboxErrorClasses('groupName');
+
+      $html.find('input').should.not.have.attr('aria-invalid')
+    });
+  });
+
   describe('resetForm()', function() {
     it('removes aria-invalid attribute from elements', function() {
       $html = $('<form data-abide><input type="text"></form>').appendTo('body');
diff --git a/test/visual/abide/abide-checkboxes.html b/test/visual/abide/abide-checkboxes.html
new file mode 100644 (file)
index 0000000..4b4e2ff
--- /dev/null
@@ -0,0 +1,107 @@
+<!doctype html>
+<!--[if IE 9]><html class="lt-ie10" lang="en" > <![endif]-->
+<html class="no-js" lang="en" dir="ltr">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <title>Foundation for Sites Testing</title>
+    <link href="../assets/css/foundation.css" rel="stylesheet" />
+  </head>
+  <body>
+    <div class="grid-container">
+      <div class="grid-x grid-padding-x">
+        <div class="cell">
+          <h1>Abide Checkboxes</h1>
+
+          <p>This form has required checkboxes.  If you try to submit without picking one, it
+          should show an error.  When you then pick one, the error should clear and let you submit.</p>
+          <form id="form" data-abide novalidate>
+            <div class="alert callout hide" data-abide-error>
+              <p>This form has errors.</p>
+            </div>
+            <fieldset>
+              <legend>Fieldset Label</legend>
+              <input required type="checkbox" name="example1" value="yes" id="example1Yes" />
+              <label for="example1Yes">Yes</label>
+              <input required type="checkbox" name="example1" value="no" id="example1No" />
+              <label for="example1No">No</label>
+            </fieldset>
+            <button class="button" type="submit">Submit</button>
+            <button class="button" type="reset">Reset</button>
+          </form>
+
+          <hr>
+
+          <p>This form has <strong>one</strong> required checkbox.  If you try to submit without picking one, it
+          should show an error.  When you then pick one, the error should clear and let you submit.</p>
+          <form id="form" data-abide novalidate>
+            <div class="alert callout hide" data-abide-error>
+              <p>This form has errors.</p>
+            </div>
+            <fieldset>
+              <legend>Fieldset Label</legend>
+              <input type="checkbox" name="example3" value="yes" id="example3Yes" />
+              <label for="example3Yes">Yes</label>
+              <input required type="checkbox" name="example3" value="no" id="example3No" />
+              <label for="example3No">No</label>
+              <input type="checkbox" name="example3" value="maybe" id="example3Maybe" />
+              <label for="example3Maybe">Maybe</label>
+            </fieldset>
+            <button class="button" type="submit">Submit</button>
+            <button class="button" type="reset">Reset</button>
+          </form>
+
+          <hr>
+
+          <p>This form has optional checkboxes.  It should let you submit with or without picking one.</p>
+          <form id="form" data-abide novalidate>
+            <div class="alert callout hide" data-abide-error>
+              <p>This form has errors.</p>
+            </div>
+            <fieldset>
+              <legend>Fieldset Label</legend>
+              <input type="checkbox" name="example2" value="yes" id="example2Yes" />
+              <label for="example2Yes">Yes</label>
+              <input type="checkbox" name="example2" value="no" id="example2No" />
+              <label for="example2No">No</label>
+            </fieldset>
+            <button class="button" type="submit">Submit</button>
+            <button class="button" type="reset">Reset</button>
+          </form>
+
+          <hr>
+
+          <p>This form has a required checkbox with a custom value for <strong>data-min-required</strong> that specifies how many checkboxes must be checked in the group. If you try to submit without checking at least 3 checkboxes, it
+          should show an error.  When you then pick the required amount, the error should clear and let you submit.</p>
+          <form id="form" data-abide novalidate>
+            <div class="alert callout hide" data-abide-error>
+              <p>This form has errors.</p>
+            </div>
+            <fieldset>
+              <legend>Fieldset Label</legend>
+              <input type="checkbox" name="example4" value="yes" id="example4Yes" />
+              <label for="example4Yes">Yes</label>
+              <input required data-min-required="3" type="checkbox" name="example4" value="no" id="example4No" />
+              <label for="example4No">No</label>
+              <input type="checkbox" name="example4" value="maybe" id="example4Maybe" />
+              <label for="example4Maybe">Maybe</label>
+              <input type="checkbox" name="example4" value="some" id="example4Some" />
+              <label for="example4Some">Some</label>
+              <input type="checkbox" name="example4" value="more" id="example4More" />
+              <label for="example4More">More</label>
+            </fieldset>
+            <button class="button" type="submit">Submit</button>
+            <button class="button" type="reset">Reset</button>
+          </form>
+
+          <hr>
+        </div>
+      </div>
+    </div>
+
+    <script src="../assets/js/vendor.js"></script>
+    <script src="../assets/js/foundation.js"></script>
+    <script>
+      $(document).foundation();
+    </script>
+  </body>
\ No newline at end of file