diff --git a/dist/demo/demo.html b/dist/demo/demo.html index cb3050f..ce90b68 100644 --- a/dist/demo/demo.html +++ b/dist/demo/demo.html @@ -11,7 +11,7 @@
-

1.0.0-13

+

1.0.0-14

Layouts

Layout Demo
diff --git a/dist/index.html b/dist/index.html index ba6a2e0..8a17f38 100644 --- a/dist/index.html +++ b/dist/index.html @@ -11,7 +11,7 @@
-

1.0.0-13

+

1.0.0-14

Home

NYCO Patterns Framework

Front-end stack, CLI, and cross-utility library for design systems. Created by NYC Opportunity for NYCO Patterns, ACCESS NYC Patterns, and Growing Up/Generation NYC Patterns.

diff --git a/dist/toggle.html b/dist/toggle.html index a292ce3..d96c1ea 100644 --- a/dist/toggle.html +++ b/dist/toggle.html @@ -11,7 +11,7 @@
-

1.0.0-13

+

1.0.0-14

Toggle

Toggle

The Toggle utility uses JavaScript to expand and collapse elements based on user interaction. This will toggle dynamic aria attributes as well as dynamic classes on both the toggling element and target of the toggle. The class “hidden” will be toggled on the target element and the class “active” will be toggled on the toggling element and target element. The target is selected using the static aria-controls attribute by its id.

@@ -21,7 +21,7 @@

JavaScript

new Toggle();

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

@@ -30,7 +30,7 @@

Markup

</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 @@ -40,24 +40,18 @@

Markup

</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 and aria-pressed would be used for toggle buttons 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.

-

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.

-
<button aria-controls="toggle-target" aria-expanded="false" data-js="toggle" type="button">
-  Toggle
-</button>
-
-<div aria-hidden="true" class="hidden" id="toggle-target">
-  <p>Targeted Toggle Element</p>
-
-  <a href='#' tabindex="-1">A Focusable Child Element</a>
-</div>
+

Element Proximity

+

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.

+

Tabindex

+

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.

+

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.

<button aria-controls="toggle-target" aria-expanded="false" data-js="toggle" type="button">
   Toggle
@@ -69,9 +63,29 @@ 

Markup

<button tabindex="-1" aria-controls="main-menu" aria-expanded="false" data-js="toggle"> Close </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

@@ -97,6 +111,18 @@

Attributes

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 @@ -214,8 +240,8 @@

Attributes

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. + 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. + @@ -247,6 +273,16 @@

Configuration

optional Full selector string of the toggle element (this is passed to the .matches() method). + + + namespace + + + string + + optional + The namespace for data selectors associated with the toggle element. Can be used in conjunction with customizing the selector. The default is toggle. Ex; the namespace in the data-toggle-tabindex selector (described above) would change the namespace in brackets; data-{{ namespace }}-tabindex. + inactiveClass @@ -275,7 +311,7 @@

Configuration

function optional - A function that will be executed before the toggling element and target classes and attributes are toggled. The function is passed the instance of the toggle class with several values that may be useful in the callback such as the settings, toggle element, toggle target, and initial click event. + A function that will be executed before the toggling element and target classes and attributes are toggled. The function is passed the instance of the toggle class with several values that may be useful in the callback such as the settings, toggle element, toggle target, and initial click event. See below for details. @@ -285,10 +321,127 @@

Configuration

function optional - A function that will be executed after the toggling element and target classes and attributes are toggled. The function is passed the instance of the toggle class with several values that may be useful in the callback such as the settings, toggle element, toggle target, and initial click event. + A function that will be executed after the toggling element and target classes and attributes are toggled. The function is passed the instance of the toggle class with several values that may be useful in the callback such as the settings, toggle element, toggle target, and initial click event. See below for details. +

+ Before/After Callback Instance Properties +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescription
+ element + + Node Element + The element that triggered the toggle.
+ event + + Click Event + The original click event.
+ focusable + + Node List + A list of elements within the toggle target that can receive focus.
+ others + + Node List + A list of other toggle elements that can trigger the toggle.
+ settings + + Object + The settings of the toggle instance.
+ target + + Node Element + The target toggle element.
+

Usage in a Pattern Module

+
'use strict';
+
+import Toggle from '@nycopportunity/patterns-framework/src/utilities/toggle/toggle';
+
+class MobileMenu {
+  constructor() {
+    this.selector = MobileMenu.selector;
+
+    this.namespace = MobileMenu.namespace;
+
+    this.selectors = MobileMenu.selectors;
+
+    this.toggle = new Toggle({
+      // Pass the pattern's custom selector
+      selector: this.selector,
+      // Pass the pattern's custom namespace
+      namespace: this.namespace,
+      // Pass a callback with functionality unique to the Mobile Menu Pattern
+      after: toggle => {
+        // Shift focus from the open to the close button in the Mobile Menu when toggled
+        if (toggle.target.classList.contains(Toggle.activeClass)) {
+          toggle.target.querySelector(this.selectors.CLOSE).focus();
+
+          // When the last focusable item in the list looses focus loop to the first
+          toggle.focusable.item(toggle.focusable.length - 1)
+            .addEventListener('blur', () => {
+              toggle.focusable.item(0).focus();
+            });
+        } else {
+          document.querySelector(this.selectors.OPEN).focus();
+        }
+      }
+    });
+
+    return this;
+  }
+}
+
+MobileMenu.selector = '[data-js*="mobile-menu"]';
+
+MobileMenu.namespace = 'mobile-menu';
+
+MobileMenu.selectors = {
+  CLOSE: '[data-js-mobile-menu*="close"]',
+  OPEN: '[data-js-mobile-menu*="open"]'
+};
+
+export default MobileMenu;

Polyfills

The script uses the Element.prototype.matches, Element.prototype.removes, Nodelist.prototype.forEach methods which require polyfills for IE11 support.

Demo

diff --git a/dist/utilities/toggle/toggle.iffe.js b/dist/utilities/toggle/toggle.iffe.js index 334484b..cae70dd 100644 --- a/dist/utilities/toggle/toggle.iffe.js +++ b/dist/utilities/toggle/toggle.iffe.js @@ -17,8 +17,8 @@ var Toggle = (function () { var Toggle = function Toggle(s) { var this$1 = this; // 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; @@ -37,26 +37,104 @@ var Toggle = (function () { this.element.addEventListener('click', function (event) { this$1.toggle(event); }); - } 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', function (event) { - if (!event.target.matches(this$1.settings.selector)) { - return; - } // Store the event for potential use in callbacks - - - this$1.event = event; - this$1.toggle(event); - }); - } // Record that a toggle using this selector has been instantiated. This + } else { + // If there isn't an existing instantiated toggle, add the event listener. + if (!window[Toggle.callback].hasOwnProperty(this.settings.selector)) { + var body = document.querySelector('body'); + + for (var i = 0; i < Toggle.events.length; i++) { + var tggleEvent = Toggle.events[i]; + body.addEventListener(tggleEvent, function (event) { + if (!event.target.matches(this$1.settings.selector)) { + return; + } + + this$1.event = event; + var type = event.type.toUpperCase(); + + if (this$1[event.type] && Toggle.elements[type] && Toggle.elements[type].includes(event.target.tagName)) { + this$1[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{Event}eventThe original click event + */ + + + Toggle.prototype.click = function click(event) { + this.toggle(event); + }; + /** + * Input/select/textarea change event handler. Checks to see if the + * event.target is valid then toggles accordingly. + * + * @param{Event}eventThe original input change event + */ + + + Toggle.prototype.change = function change(event) { + var valid = event.target.checkValidity(); + + 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}elThe toggle element (trigger) + */ + + + Toggle.prototype.isActive = function isActive(el) { + var 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}elThe toggle element (trigger) + */ + + + Toggle.prototype.getTarget = function getTarget(el) { + var target = false; + /** Anchor Links */ + + target = el.hasAttribute('href') ? document.querySelector(el.getAttribute('href')) : target; + /** Toggle Controls */ + + 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}eventThe main click event * @@ -70,12 +148,7 @@ var Toggle = (function () { var target = false; var focusable = []; event.preventDefault(); - /** Anchor Links */ - - target = el.hasAttribute('href') ? document.querySelector(el.getAttribute('href')) : target; - /** Toggle Controls */ - - target = el.hasAttribute('aria-controls') ? document.querySelector("#" + el.getAttribute('aria-controls')) : target; + target = this.getTarget(el); /** Focusable Children */ focusable = target ? target.querySelectorAll(Toggle.elFocusable.join(', ')) : focusable; @@ -100,7 +173,7 @@ var Toggle = (function () { return this; }; /** - * 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 @@ -238,7 +311,7 @@ var Toggle = (function () { return this; }; - /** @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"]'; @@ -260,6 +333,18 @@ var Toggle = (function () { /** @type {Array} Focusable elements to hide within the hidden target element */ Toggle.elFocusable = ['a', 'button', 'input', 'select', 'textarea', 'object', 'embed', 'form', 'fieldset', 'legend', 'label', 'area', 'audio', 'video', 'iframe', 'svg', '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'] + }; return Toggle; diff --git a/package-lock.json b/package-lock.json index 56658ac..15c9bb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@nycopportunity/patterns-framework", - "version": "1.0.0-13", + "version": "1.0.0-14", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0a85a30..8872fcd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@nycopportunity/patterns-framework", "nice": "NYCO Patterns Framework", - "version": "1.0.0-13", + "version": "1.0.0-14", "description": "Front-end utilities from the Mayor's Office for Economic Opportunity", "author": "NYC Opportunity", "license": "GPL-3.0+", diff --git a/src/config/_tokens.scss b/src/config/_tokens.scss index 16ba1b2..5e40b41 100644 --- a/src/config/_tokens.scss +++ b/src/config/_tokens.scss @@ -6,5 +6,5 @@ $tokens:( speed: 0.75s, timing-function: cubic-bezier(0.23, 1, 0.32, 1) ), - version: "1.0.0-13" + version: "1.0.0-14" );