Skip to content

Commit

Permalink
Use pull request foundation#10699 from Owlbertz/abide-accessibility f…
Browse files Browse the repository at this point in the history
…or v6.5.0

51a2558 Implemented accessibility for Abide.
dfae510 Improved accessibility of Abide page.
3e512b9 Added unittests for aria-invalid in Abide.
835cf04 docs: improve accessibility of abide examples
07b2d76 feat: add [aria-live] on [data-abide-error] on form validation
88abca7 feat: automatically add [aria-describedby] and [for] attributes in Abide
d1acd32 feat: add `a11yAttributes` option on Abide to disable a11y attribute insertion
f55efd5 feat: set `[aria-live]` on Abide global erros on init
6cf6619 docs: add basic doc about a11y attributes in Abide
32efbc2 test: add unit tests for Abide.addA11yAttributes()
8e7fa06 test: add unit test for Abide.addGlobalErrorA11yAttributes()
edf21d2 test: add visual test for Abide accessibility
c6a6906 fix: fix side-effect of jQuery .filter() in Abide.addA11yAttributes
c6e4b56 feat: add [role=label] to all form errors in Abide

Co-Authored-By: Nicolas Coden <nicolas@ncoden.fr>
Signed-off-by: Nicolas Coden <nicolas@ncoden.fr>
  • Loading branch information
Owlbertz and ncoden committed Jun 16, 2018
1 parent 19362bb commit 39bea9a
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 37 deletions.
71 changes: 43 additions & 28 deletions docs/pages/abide.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,30 @@ These input types create a text field: `text`, `date`, `datetime`, `datetime-loc
<div class="row">
<div class="small-12 columns">
<label>Number Required
<input type="text" placeholder="1234" aria-describedby="exampleHelpText" required pattern="number">
<span class="form-error">
<input type="text" placeholder="1234" aria-describedby="example1Hint1" aria-errormessage="example1Error1" required pattern="number">
<span class="form-error" id="example1Error1">
Yo, you had better fill this out, it's required.
</span>
</label>
<p class="help-text" id="exampleHelpText">Here's how you use this input field!</p>
<p class="help-text" id="example1Hint1">Here's how you use this input field!</p>
</div>
<div class="small-12 columns">
<label>Password Required
<input type="password" id="password" placeholder="yeti4preZ" aria-describedby="exampleHelpText" required >
<span class="form-error">
<input type="password" id="password" placeholder="yeti4preZ" aria-describedby="example1Hint2" aria-errormessage="example1Error2" required >
<span class="form-error" id="example1Error2">
I'm required!
</span>
</label>
<p class="help-text" id="exampleHelpText">Enter a password please.</p>
<p class="help-text" id="example1Hint2">Enter a password please.</p>
</div>
<div class="small-12 columns">
<label>Re-enter Password
<input type="password" placeholder="yeti4preZ" aria-describedby="exampleHelpText2" required pattern="alpha_numeric" data-equalto="password">
<span class="form-error">
<input type="password" placeholder="yeti4preZ" aria-describedby="example1Hint3" aria-errormessage="example1Error3" required pattern="alpha_numeric" data-equalto="password">
<span class="form-error" id="example1Error3">
Hey, passwords are supposed to match!
</span>
</label>
<p class="help-text" id="exampleHelpText2">This field is using the `data-equalto="password"` attribute, causing it to match the password field above.</p>
<p class="help-text" id="example1Hint3">This field is using the `data-equalto="password"` attribute, causing it to match the password field above.</p>
</div>
</div>
<div class="row">
Expand Down Expand Up @@ -120,15 +120,15 @@ These input types create a text field: `text`, `date`, `datetime`, `datetime-loc

<label class="is-invalid-label">
Required Thing
<input type="text" class="is-invalid-input">
<span class="form-error is-visible">
<input type="text" class="is-invalid-input" aria-describedby="exemple2Error" data-invalid aria-invalid="true">
<span class="form-error is-visible" id="exemple2Error">
Yo, you had better fill this out.
</span>
</label>

<label class="is-invalid-label">
Required Thing
<textarea type="text" class="is-invalid-input"></textarea>
<textarea type="text" class="is-invalid-input" data-invalid aria-invalid="true"></textarea>
</label>

---
Expand All @@ -141,14 +141,19 @@ When the Form Errors cannot be placed next to its field, like in an Input Group,

```html_example
<form data-abide novalidate>
<label>
<div data-abide-error class="sr-only">
There are some errors in your form.
</div>
<div>
Amount
<div class="input-group">
<span class="input-group-label">$</span>
<input class="input-group-field" id="exampleNumberInput" type="number" required pattern="number"/>
<input class="input-group-field" id="example3Input" type="number" required pattern="number"/>
</div>
<span class="form-error" data-form-error-for="exampleNumberInput">Amount is required.</span>
</label>
<label class="form-error" data-form-error-for="example3Input">Amount is required.</label>
</div>
<button class="button" type="submit" value="Submit">Submit</button>
</form>
```
Expand All @@ -159,13 +164,13 @@ When the Form Errors cannot be placed next to its field, like in an Input Group,
```html
<form data-abide>
<!-- Add "display: none" right away -->
<div data-abide-error class="alert callout" style="display: none;">
<div data-abide-error class="alert callout" aria-live="assertive" style="display: none;">
<p><i class="fi-alert"></i> There are some errors in your form.</p>
</div>
<label>
Name
<input type="text" required>
<span class="form-error">This field is required.</span>
<input id="example4Input" aria-describedby="example4Error" type="text" required>
<span id="example4Error" class="form-error">This field is required.</span>
</label>
</form>
```
Expand All @@ -176,41 +181,45 @@ When the Form Errors cannot be placed next to its field, like in an Input Group,
<form data-abide>
<!-- Add role="alert" -->
<!-- Add "display: block" -->
<div data-abide-error role="alert" class="alert callout" style="display: block;">
<div data-abide-error class="alert callout" aria-live="assertive" role="alert" style="display: block;">
<p><i class="fi-alert"></i> There are some errors in your form.</p>
</div>
<!-- Add "is-invalid-label" -->
<label class="is-invalid-label">
Name
<!-- Add "is-invalid-input" -->
<input type="text" class="is-invalid-input" required aria-invalid aria-describedby="uuid">
<!-- Add aria-invalid="true" -->
<input id="example4Input" aria-describedby="example4Error" type="text" required
class="is-invalid-input" aria-invalid="true">
<!-- Add "is-visible" -->
<span class="form-error is-visible" id="uuid">This field is required.</span>
<span id="example4Error" class="form-error is-visible">This field is required.</span>
</label>
</form>
```

---

## Ignored Inputs

```html
<form data-abide>
<div class="small-12 columns">
<label>Nothing Required!
<input type="text" placeholder="Use me, or don't" aria-describedby="exampleHelpTex" data-abide-ignore>
<input type="text" placeholder="Use me, or don't" aria-describedby="example5Hint1" data-abide-ignore>
</label>
<p class="help-text" id="exampleHelpTex">This input is ignored by Abide using `data-abide-ignore`</p>
<p class="help-text" id="example5Hint1">This input is ignored by Abide using `data-abide-ignore`</p>
</div>
<div class="small-12 columns">
<label>Disabled!
<input type="text" placeholder="Disabled input" aria-describedby="exampleHelpTex" disabled>
<input type="text" placeholder="Disabled input" aria-describedby="example5Hint2" disabled>
</label>
<p class="help-text" id="exampleHelpTex">This input is ignored by Abide using `disabled`</p>
<p class="help-text" id="example5Hint2">This input is ignored by Abide using `disabled`</p>
</div>
<div class="small-12 columns">
<label>Hidden!
<input type="hidden" placeholder="Hidden input" aria-describedby="exampleHelpTex" >
<input type="hidden" placeholder="Hidden input" aria-describedby="example5Hint3" >
</label>
<p class="help-text" id="exampleHelpTex">This input is ignored by Abide using `type="hidden"`</p>
<p class="help-text" id="example5Hint3">This input is ignored by Abide using `type="hidden"`</p>
</div>
<div class="row">
<fieldset class="large-6 columns">
Expand Down Expand Up @@ -366,3 +375,9 @@ function($el,required,parent) {
<input id="min" type="number" required >
<input id="max" type="number" data-validator="greater_than" data-greater-than="min" required>
```
## Accessibility
By default, Abide will add some accessibility attributes to your form elements. It is highly recommended to keep this option active as it improve the usability of your forms for disabled people. [Lean more about Accessibility in Foundation](accessibility.html).
However, if you think the attributes added by Abide are not correct, you can disable it by setting `a11yAttributes` (or `[data-a11y-attributes]`) to `false`.
2 changes: 1 addition & 1 deletion js/entries/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Foundation.addToJquery($);
Foundation.rtl = CoreUtils.rtl;
Foundation.GetYoDigits = CoreUtils.GetYoDigits;
Foundation.transitionend = CoreUtils.transitionend;
Foundation.transitionend = RegExpEscape;
Foundation.RegExpEscape = RegExpEscape;

Foundation.Box = Box;
Foundation.onImagesLoaded = onImagesLoaded;
Expand Down
124 changes: 116 additions & 8 deletions js/foundation.abide.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import $ from 'jquery';
import { Plugin } from './foundation.plugin';
import { GetYoDigits } from './foundation.util.core';

/**
* Abide module.
Expand Down Expand Up @@ -31,6 +32,13 @@ class Abide extends Plugin {
*/
_init() {
this.$inputs = this.$element.find('input, textarea, select');
const $globalErrors = this.$element.find('[data-abide-error]');

// Add a11y attributes to all fields
if (this.options.a11yAttributes) {
this.$inputs.each((i, input) => this.addA11yAttributes($(input)));
$globalErrors.each((i, error) => this.addGlobalErrorA11yAttributes($(error)));
}

this._events();
}
Expand Down Expand Up @@ -192,15 +200,73 @@ class Abide extends Plugin {
$formError.addClass(this.options.formErrorClass);
}

$el.addClass(this.options.inputErrorClass).attr('data-invalid', '');
$el.addClass(this.options.inputErrorClass).attr({
'data-invalid': '',
'aria-invalid': true
});
}

/**
* Adds [for] and [role=alert] attributes to all form error targetting $el,
* and [aria-describedby] attribute to $el toward the first form error.
* @param {Object} $el - jQuery object
*/
addA11yAttributes($el) {
let $errors = this.findFormError($el);
let $labels = $errors.filter('label').end();
let $error = $errors.first().end();
if (!$errors.length) return;

// Set [aria-describedby] on the input toward the first form error if it is not set
if (typeof $el.attr('aria-describedby') === 'undefined') {
// Get the first error ID or create one
let errorId = $error.attr('id');
if (typeof errorId === 'undefined') {
errorId = GetYoDigits(6, 'abide-error');
$error.attr('id', errorId);
};

$el.attr('aria-describedby', errorId);
}

if ($labels.filter('[for]').end().length < $labels.length) {
// Get the input ID or create one
let elemId = $el.attr('id');
if (typeof elemId === 'undefined') {
elemId = GetYoDigits(6, 'abide-input');
$el.attr('id', elemId);
};

// For each label targeting $el, set [for] if it is not set.
$labels.each((i, label) => {
const $label = $(label);
if (typeof $label.attr('for') === 'undefined')
$label.attr('for', elemId);
}).end();
}

// For each error targeting $el, set [role=alert] if it is not set.
$errors.each((i, label) => {
const $label = $(label);
if (typeof $label.attr('role') === 'undefined')
$label.attr('role', 'alert');
}).end();
}

/**
* Adds [aria-live] attribute to the given global form error $el.
* @param {Object} $el - jQuery object to add the attribute to
*/
addGlobalErrorA11yAttributes($el) {
if (typeof $el.attr('aria-live') === 'undefined')
$el.attr('aria-live', this.options.a11yErrorLevel);
}

/**
* Remove CSS error classes etc from an entire radio button group
* @param {String} groupName - A string that specifies the name of a radio button group
*
*/

removeRadioErrorClasses(groupName) {
var $els = this.$element.find(`:radio[name="${groupName}"]`);
var $labels = this.findRadioLabels($els);
Expand All @@ -214,7 +280,10 @@ class Abide extends Plugin {
$formErrors.removeClass(this.options.formErrorClass);
}

$els.removeClass(this.options.inputErrorClass).removeAttr('data-invalid');
$els.removeClass(this.options.inputErrorClass).attr({
'data-invalid': null,
'aria-invalid': null
});

}

Expand All @@ -239,7 +308,10 @@ class Abide extends Plugin {
$formError.removeClass(this.options.formErrorClass);
}

$el.removeClass(this.options.inputErrorClass).removeAttr('data-invalid');
$el.removeClass(this.options.inputErrorClass).attr({
'data-invalid': null,
'aria-invalid': null
});
}

/**
Expand Down Expand Up @@ -335,7 +407,13 @@ class Abide extends Plugin {

var noError = acc.indexOf(false) === -1;

this.$element.find('[data-abide-error]').css('display', (noError ? 'none' : 'block'));
this.$element.find('[data-abide-error]').each((i, elem) => {
const $elem = $(elem);
// Ensure a11y attributes are set
if (this.options.a11yAttributes) this.addGlobalErrorA11yAttributes($elem);
// Show or hide the error
$elem.css('display', (noError ? 'none' : 'block'));
});

/**
* Fires when the form is finished validating. Event trigger is either `formvalid.zf.abide` or `forminvalid.zf.abide`.
Expand Down Expand Up @@ -440,9 +518,18 @@ class Abide extends Plugin {
$(`.${opts.inputErrorClass}`, $form).not('small').removeClass(opts.inputErrorClass);
$(`${opts.formErrorSelector}.${opts.formErrorClass}`).removeClass(opts.formErrorClass);
$form.find('[data-abide-error]').css('display', 'none');
$(':input', $form).not(':button, :submit, :reset, :hidden, :radio, :checkbox, [data-abide-ignore]').val('').removeAttr('data-invalid');
$(':input:radio', $form).not('[data-abide-ignore]').prop('checked',false).removeAttr('data-invalid');
$(':input:checkbox', $form).not('[data-abide-ignore]').prop('checked',false).removeAttr('data-invalid');
$(':input', $form).not(':button, :submit, :reset, :hidden, :radio, :checkbox, [data-abide-ignore]').val('').attr({
'data-invalid': null,
'aria-invalid': null
});
$(':input:radio', $form).not('[data-abide-ignore]').prop('checked',false).attr({
'data-invalid': null,
'aria-invalid': null
});
$(':input:checkbox', $form).not('[data-abide-ignore]').prop('checked',false).attr({
'data-invalid': null,
'aria-invalid': null
});
/**
* Fires when the form has been reset.
* @event Abide#formreset
Expand Down Expand Up @@ -514,6 +601,27 @@ Abide.defaults = {
*/
formErrorClass: 'is-visible',

/**
* If true, automatically insert when possible:
* - `[aria-describedby]` on fields
* - `[role=alert]` on form errors and `[for]` on form error labels
* - `[aria-live]` on global errors `[data-abide-error]` (see option `a11yErrorLevel`).
* @option
* @type {boolean}
* @default true
*/
a11yAttributes: true,

/**
* [aria-live] attribute value to be applied on global errors `[data-abide-error]`.
* Options are: 'assertive', 'polite' and 'off'/null
* @option
* @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions
* @type {string}
* @default 'assertive'
*/
a11yErrorLevel: 'assertive',

/**
* Set to true to validate text inputs on any value change.
* @option
Expand Down
Loading

0 comments on commit 39bea9a

Please sign in to comment.