Skip to content

Commit

Permalink
Toggle handlers for click and change events. Updated documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
devowhippit committed Jul 22, 2020
1 parent 4e1cf64 commit 6dcc1f4
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 55 deletions.
79 changes: 50 additions & 29 deletions src/utilities/toggle/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The Toggle utility uses JavaScript to expand and collapse elements based on user

### Markup

Elements should have the hidden or active state set before initialization.
Elements must have the hidden or active state classes and attributes set before initialization.

**Hidden**

Expand All @@ -21,7 +21,7 @@ Elements should have the hidden or active state set before initialization.
</button>

<div aria-hidden="true" class="hidden" id="toggle-target">
<p>Targeted Toggle Element</p>
<p>Targeted toggle element. <a href='#' tabindex='-1'>A focusable child element</a>.</p>
</div>

**Active**
Expand All @@ -31,24 +31,20 @@ Elements should have the hidden or active state set before initialization.
</button>

<div aria-hidden="false" class="active" id="toggle-target">
<p>Targeted Toggle Element</p>
<p>Targeted toggle element. <a href='#'>A focusable child element</a>.</p>
</div>

The use of the dynamic `aria-expanded` attribute on the toggling element is recommended for toggling elements as it will announce that the target of the toggle is "expanded" or "collapsed." Optionally, the attribute `aria-pressed` can be used instead to announce that the toggle button is "pressed" or "not pressed". These attributes provide different feedback to screenreaders and are appropriate for different component types. `aria-expanded` would be used for patterns such as [**collapsible sections**](https://inclusive-components.design/collapsible-sections/) and `aria-pressed` would be used for [**toggle buttons**](https://inclusive-components.design/toggle-button/) or **switches**. A full list of dynamic and static attributes is described below.

Placement of the target should follow the toggling element so that it appears next in order on the page for screen readers. For targets that are far apart or appear in a different section of the page, the Anchor Toggle may be more appropriate.
### Element Proximity

Elements that have aria-hidden set to `true` should not contain focusable elements. Setting their tabindex to `-1` will prevent them from being focused on. For convenience, child elements in the target element that have their `tabindex` set will be toggled.
Placement of the target should follow the toggling element so that it appears next in order on the page for screen readers. For targets that are far apart or appear in a different section of the page, the Anchor Toggle may be more appropriate as it will shift focus to the target.

<button aria-controls="toggle-target" aria-expanded="false" data-js="toggle" type="button">
Toggle
</button>
### Tabindex

<div aria-hidden="true" class="hidden" id="toggle-target">
<p>Targeted Toggle Element</p>
Elements that have aria-hidden set to `true` should not contain focusable elements. Setting their tabindex to `-1` will prevent them from being focused on. For convenience, child elements in the target element will have their `tabindex` toggled. Refer to the full list of potentially focusable elements below that will be toggled.

<a href='#' tabindex="-1">A Focusable Child Element</a>
</div>
### Multiple Toggle Elements (Triggers)

The Toggle Utility supports having more than one toggle element per toggle target. An example use case is for "close" buttons within dialogue elements.

Expand All @@ -64,34 +60,59 @@ The Toggle Utility supports having more than one toggle element per toggle targe
</button>
</div>

### Form Elements

In addition to listening to "click" events on `<a>` and `<button>` tags the utility will listen to the "change" event on form elements: `<input>`, `<select>`, and `<textarea>`. Their targets will toggled based on passing HTML5 form validation of the element. The target will only be toggled if the form element has a value when it is required or if the value matches a required pattern.

<label for="question-1">Question 1</label>

<select id="question-1" name="question[1]" aria-controls="next-question" aria-expanded="false" required="true">
<option value="">Please select 1 or 2</option>
<option value="option-1">Option 1</option>
<option value="option-2">Option 2</option>
</select>

<div id="next-question" class="hidden" aria-expanded="false">
<label for="question-2">Question 2</label>

<select id="question-2" name="question[2]" tabindex="-1">
<option value="">Please select A or B</option>
<option value="option-a">Option A</option>
<option value="option-b">Option B</option>
</select>
</div>

### Attributes

Attributes on the Element, Target, and Target Children, such as `aria-hidden`, `aria-controls`, `aria-expanded`, `type`, and `tabindex` help assistive technologies understand the relationship between each element and their respective states of visibility. These attributes should be present but they may be interchanged with others based on the use case. Below is an explanation of all attributes that can be used with the toggle utility. *Static* attributes will not change. *Dynamic* attributes will change when the toggle event is fired.
Attributes on the Element, Target, and the Target's children, such as `aria-hidden`, `aria-controls`, `aria-expanded`, `type`, and `tabindex` help assistive technologies understand the relationship between each element and their respective states of visibility. Some attributes are required on the element on page load.

Some attributes may be interchanged with others based on the use case. Below is an explanation of all attributes that can be used with the toggle utility. *Static* attributes will not change. *Dynamic* attributes will change when the toggle event is fired.

**Toggling Element Attributes**

Attribute | State | Importance | Description
----------------|-----------|---------------|-
`aria-controls` | *static* | **required** | ID of the target element. Used by the toggle to select the target element.
`aria-expanded` | *dynamic* | recommended | Boolean that announces that target content is "expanded" or "collapsed" when the toggling element is clicked.
`type` | *static* | recommended | Setting a `<button>` element type to "button" will distinguish it from other button types, such as "submit" and "reset," but only within `<form>` elements. By default, a `<button>` is the type "submit" within a form.
`aria-pressed` | *dynamic* | optional | Boolean that announces that the toggling element is toggled. Not recommended for use with `aria-expanded`. Commonly used with buttons that act as switches for options that are on or off.
`role` | *static* | optional | If the toggling element is not a `<button>` element, but looks and behaves like a button (see documentation for the [Button Element](/buttons)), then setting the `role` attribute to "button" is recommended. See [MDN documentation for the "button" role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role) for more information
Attribute | State | Importance | Description
----------------|-----------|--------------|-
`aria-controls` | *static* | **required** | ID of the target element. Used by the toggle to select the target element.
`tabindex` | *dynamic* | **required** | If a child element of the target element is potentially focusable it's tabindex will be toggled to `-1` to prevent it's visibility from screen readers. Refer to the list below of potentially focusable elements that will be toggled.
`aria-expanded` | *dynamic* | recommended | Boolean that announces that target content is "expanded" or "collapsed" when the toggling element is clicked.
`type` | *static* | recommended | Setting a `<button>` element type to "button" will distinguish it from other button types, such as "submit" and "reset," but only within `<form>` elements. By default, a `<button>` is the type "submit" within a form.
`aria-pressed` | *dynamic* | optional | Boolean that announces that the toggling element is toggled. Not recommended for use with `aria-expanded`. Commonly used with buttons that act as switches for options that are on or off.
`role` | *static* | optional | If the toggling element is not a `<button>` element, but looks and behaves like a button (see documentation for the [Button Element](/buttons)), then setting the `role` attribute to "button" is recommended. See [MDN documentation for the "button" role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role) for more information

**Target Element Attributes**

Attribute | State | Importance | Description
------------------|-----------|---------------|-
`aria-hidden` | *dynamic* | recommended | Boolean that hides the content of the target element when "collapsed."
`role` | *static* | optional | Setting the target element's `role` to "region" identifies the target as a significant area. See [MDN documentation for the "region" role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Region_role) for more information.
`aria-labelledby` | *static* | optional | This is used along with the `role` attribute to label the content of a "region." This can be set to the toggling elements `id` but can also be set to a different elements `id`.
Attribute | State | Importance | Description
------------------|-----------|-------------|-
`aria-hidden` | *dynamic* | recommended | Boolean that hides the content of the target element when "collapsed."
`role` | *static* | optional | Setting the target element's `role` to "region" identifies the target as a significant area. See [MDN documentation for the "region" role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Region_role) for more information.
`aria-labelledby` | *static* | optional | This is used along with the `role` attribute to label the content of a "region." This can be set to the toggling elements `id` but can also be set to a different elements `id`.

**Target Element Child Attributes**

Attribute | State | Importance | Description
-----------------------|-----------|---------------|-
`tabindex` | *dynamic* | recommended | Setting the toggle target's focusable children's `tabindex` attribute to "-1" will prevent them from being focused on when the parent is hidden. See the list below of potentially focusable elements that are supported.
`data-toggle-tabindex` | *static* | optional | If an child element has a `tabindex` that needs to be set when the parent target is visible then the default value can be stored in this data attribute.
Attribute | State | Importance | Description
-----------------------|-----------|-------------|-
`tabindex` | *dynamic* | recommended | Setting the toggle target's focusable children's `tabindex` attribute to "-1" will prevent them from being focused on when the parent is hidden. See the list below of potentially focusable elements that are supported.
`data-toggle-tabindex` | *static* | optional If an child element has a `tabindex` that needs to be set when the parent target is visible then the default value can be stored in this data attribute.

**Potentially Focusable Elements**

Expand Down
141 changes: 115 additions & 26 deletions src/utilities/toggle/toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class Toggle {
*/
constructor(s) {
// Create an object to store existing toggle listeners (if it doesn't exist)
if (!window.hasOwnProperty('ACCESS_TOGGLES'))
window.ACCESS_TOGGLES = [];
if (!window.hasOwnProperty(Toggle.callback))
window[Toggle.callback] = [];

s = (!s) ? {} : s;

Expand All @@ -39,43 +39,100 @@ class Toggle {
// Store the element for potential use in callbacks
this.element = (s.element) ? s.element : false;

if (this.element)
if (this.element) {
this.element.addEventListener('click', (event) => {
this.toggle(event);
});
else
} else {
// If there isn't an existing instantiated toggle, add the event listener.
if (!window.ACCESS_TOGGLES.hasOwnProperty(this.settings.selector))
document.querySelector('body').addEventListener('click', event => {
if (!event.target.matches(this.settings.selector))
return;
if (!window[Toggle.callback].hasOwnProperty(this.settings.selector)) {
let body = document.querySelector('body');

// Store the event for potential use in callbacks
this.event = event;
for (let i = 0; i < Toggle.events.length; i++) {
let tggleEvent = Toggle.events[i];

this.toggle(event);
});
body.addEventListener(tggleEvent, event => {
if (!event.target.matches(this.settings.selector))
return;

this.event = event;

let type = event.type.toUpperCase();

if (
this[event.type] &&
Toggle.elements[type] &&
Toggle.elements[type].includes(event.target.tagName)
) this[event.type](event);
});
}
}
}

// Record that a toggle using this selector has been instantiated. This
// prevents double toggling.
window.ACCESS_TOGGLES[this.settings.selector] = true;
window[Toggle.callback][this.settings.selector] = true;

return this;
}

/**
* Logs constants to the debugger
* Click event handler
*
* @param {Object} event The main click event
* @param {Event} event The original click event
*/
click(event) {
this.toggle(event);
}

/**
* Input/select/textarea change event handler. Checks to see if the
* event.target is valid then toggles accordingly.
*
* @return {Object} The class
* @param {Event} event The original input change event
*/
toggle(event) {
let el = event.target;
let target = false;
let focusable = [];
change(event) {
let valid = event.target.checkValidity();

event.preventDefault();
if (valid && !this.isActive(event.target)) {
this.toggle(event); // show
} else if (!valid && this.isActive(event.target)) {
this.toggle(event); // hide
}
}

/**
* Check to see if the toggle is active
*
* @param {Object} el The toggle element (trigger)
*/
isActive(el) {
let active = false;

if (this.settings.activeClass) {
active = el.classList.contains(this.settings.activeClass)
}

// if () {
// Toggle.elementAriaRoles
// Add catch to see if element aria roles are toggled
// }

// if () {
// Toggle.targetAriaRoles
// Add catch to see if target aria roles are toggled
// }

return active;
}

/**
* Get the target of the toggle element (trigger)
*
* @param {Object} el The toggle element (trigger)
*/
getTarget(el) {
let target = false;

/** Anchor Links */
target = (el.hasAttribute('href')) ?
Expand All @@ -85,6 +142,25 @@ class Toggle {
target = (el.hasAttribute('aria-controls')) ?
document.querySelector(`#${el.getAttribute('aria-controls')}`) : target;

return target;
}

/**
* The toggle event proxy for getting and setting the element/s and target
*
* @param {Object} event The main click event
*
* @return {Object} The class
*/
toggle(event) {
let el = event.target;
let target = false;
let focusable = [];

event.preventDefault();

target = this.getTarget(el);

/** Focusable Children */
focusable = (target) ?
target.querySelectorAll(Toggle.elFocusable.join(', ')) : focusable;
Expand All @@ -93,7 +169,7 @@ class Toggle {
if (!target) return this;
this.elementToggle(el, target, focusable);

/** Undo - may deprecate */
/** Undo */
if (el.dataset[`${this.settings.namespace}Undo`]) {
const undo = document.querySelector(
el.dataset[`${this.settings.namespace}Undo`]
Expand All @@ -110,7 +186,7 @@ class Toggle {
}

/**
* The main toggling method
* The main toggling method for attributes
*
* @param {Object} el The current element to toggle active
* @param {Object} target The target element to toggle active/hidden
Expand Down Expand Up @@ -174,7 +250,7 @@ class Toggle {
let tabindex = el.getAttribute('tabindex');

if (tabindex === '-1') {
let dataDefault = el.getAttribute(`data-${this.settings.namespace}-tabindex`);
let dataDefault = el.getAttribute(`data-${Toggle.namespace}-tabindex`);

if (dataDefault) {
el.setAttribute('tabindex', dataDefault);
Expand All @@ -201,8 +277,9 @@ class Toggle {

target.setAttribute('tabindex', '-1');
target.focus({preventScroll: true});
} else
} else {
target.removeAttribute('tabindex');
}
}

/**
Expand Down Expand Up @@ -231,7 +308,7 @@ class Toggle {
}
}

/** @type {String} The main selector to add the toggling function to */
/** @type {String} The main selector to add the toggling function to */
Toggle.selector = '[data-js*="toggle"]';

/** @type {String} The namespace for our data attribute settings */
Expand All @@ -256,4 +333,16 @@ Toggle.elFocusable = [
'details', 'table', '[tabindex]', '[contenteditable]', '[usemap]'
];

/** @type {Array} Key attribute for storing toggles in the window */
Toggle.callback = ['TogglesCallback'];

/** @type {Array} Default events to to watch for toggling. Each must have a handler in the class and elements to look for in Toggle.elements */
Toggle.events = ['click', 'change'];

/** @type {Array} Elements to delegate to each event handler */
Toggle.elements = {
CLICK: ['A', 'BUTTON'],
CHANGE: ['SELECT', 'INPUT', 'TEXTAREA']
};

export default Toggle;

0 comments on commit 6dcc1f4

Please sign in to comment.