diff --git a/docs/cookie-banner.html b/docs/cookie-banner.html deleted file mode 100644 index 2026a660..00000000 --- a/docs/cookie-banner.html +++ /dev/null @@ -1,421 +0,0 @@ - - - - StormId Components - cookie-banner - - - - -
- - -
-
- - - -

Cookie banner

-

GDPR compliant cookie banner and consent form.

-

Renders a cookie banner and a consent form based on configuration settings, and conditionally invokes cookie-reliant functionality based on user consent.

-
-

Usage

-

Cookie consent is based on categorising cookies and the functions that initialise them, describing them in a configuration object passed into the module at initialisition.

-

The cookie banner renders itself if no consent preferences are recorded in the browser.

-

The consent form renders into a DOMElement with a particular className configurable options (classNames.formContainer).

-

A page containing a cookie consent form should include a visually hidden live region (role=alert) with a particular className (classNames.formAnnouncement), default: ‘privacy-banner__form-announcement’.

-

Optionally the banner also supports basic Google EU consent mode [https://developers.google.com/tag-platform/security/guides/consent?consentmode=basic], and can push user consent preferences to the dataLayer for Google libraries to use. All that is necessary to suport Google consent mode is to map Google consent categories to the cookie categories in the configuration.

-

For example, to map the ad_storage, ad_user_data, and ad_personalisation to an ‘ads’ consent category defined in the banner config, add a euConsentTypes object to the configuration like this:

-
euConsentTypes: {
-    ad_storage: 'test',
-    ad_user_data: 'test',
-    ad_personalization: 'test'
-}
-
-

Install the package

-
npm i -S @stormid/cookie-banner
-
-

Create a container element for the consent form.

-
<div class="privacy-banner__form-container"></div>
-
-

Create a visually hidden live region for the screen reader announcement.

-
<div class="visually-hidden privacy-banner__form-announcement" role="alert"></div>
-
-

Initialise the module (example configuration shown below)

-
import banner from '@stormid/cookie-banner';
-
-const cookieBanner = banner({
-    types: {
-        'performance': {
-            suggested: true, //set as pre-checked on consent form as a suggested response
-            title: 'Performance preferences',
-            description: 'Performance cookies are used to measure the performance of our website and make improvements. Your personal data is not identified.',
-            labels: {
-                yes: 'Pages you visit and actions you take will be measured and used to improve the service',
-                no: 'Pages you visit and actions you take will not be measured and used to improve the service'
-            },
-            fns: [
-                state => { 
-                    //function that depends upon or creates a 'performance' cookie
-                },
-                state => state.utils.gtmSnippet(<UA-CODE>)
-            ]
-        },
-        'thirdParty': {
-            title: 'Third party preferences',
-            description: 'We work with third party partners to show you ads for our products and services across the web, and to serve video and audio content.  You can choose whether we collect and share that data with our partners below. ',
-            labels: {
-                yes: 'Our partners might know you have visited our website',
-                no: 'Our partners will will not know you have visited out website but you cannot video third party video and audio content'
-            },
-            fns: [
-                model => { 
-                    //function that depends upon or creates a 'performance' cookie
-                },
-                state => state.utils.renderIframe(),
-                state => state.utils.gtmSnippet(<UA-CODE>)
-            ]
-        }
-    }
-});
-
-

Options

-
{
-    name: '.CookiePreferences', //name of the cookie set to record user consent
-    path: '/', //path of the preferences cookie
-    domain: window.location.hostname === 'localhost' ? '' : `.${removeSubdomain(window.location.hostname)}`, //domain of the preferences cookie, defaults to .<root-domain>
-    secure: true, //preferences cookie secure
-    samesite: 'lax', //preferences cookie samesite
-    expiry: 365, //preferences cookie expiry in days
-    types: {}, //types of cookie-dependent functionality
-    euConsentTypes: {}, //map Google EU consent categories to types of cookie defined in 'types'
-    necessary: [], //cookie-dependent functionality that will always execute, for convenience only
-    policyURL: '/cookie-policy#preferences', //URL to cookie policy page (location of cookie consent form) rendered in the banner
-    classNames: {
-        banner: 'privacy-banner',
-        acceptBtn: 'privacy-banner__accept',
-        rejectBtn: 'privacy-banner__reject',
-        submitBtn: 'privacy-banner__submit',
-        field: 'privacy-banner__field',
-        form: 'privacy-banner__form',
-        fieldset: 'privacy-banner__fieldset',
-        legend: 'privacy-banner__legend',
-        formContainer: 'privacy-banner__form-container', //where the form is rendered
-        formMessage: 'privacy-banner__form-msg',
-        formAnnouncement: 'privacy-banner__form-announcement', //screen reader announcement
-        title: 'privacy-banner__form-title',
-        description: 'privacy-banner__form-description'
-    },
-    hideBannerOnFormPage: false, //don't show the banner when the user is on the same page as a consent form
-    savedMessage: 'Your settings have been saved.', //displayed after consent form update,
-    trapTab: false, //trap the user's keyboard tab within the banner when open
-    bannerTemplate(model){
-        return `<section role="dialog" aria-live="polite" aria-label="Your privacy" class="${model.classNames.banner}">
-            <div class="privacy-content">
-                <div class="wrap">
-                    <!--googleoff: all-->
-                    <div class="privacy-banner__title">Cookies</div>
-                    <p>We use cookies to improve your experience on our site and show you personalised advertising.</p>
-                    <p>Find out more from our <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="/privacy-policy">privacy policy</a> and <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="${model.policyURL}">cookie policy</a>.</p>
-                    <button class="btn btn--primary ${model.classNames.acceptBtn}">Accept and close</button>
-                    <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="${model.policyURL}">Your options</a>
-                    <!--googleon: all-->
-                </div>
-            </div>
-        </section>`;
-    },
-    messageTemplate(model){
-        return `<div class="${model.settings.classNames.formMessage}" aria-role="alert">${model.settings.savedMessage}</div>`
-    },
-    formTemplate(model){
-        return `<form id="preferences" class="${model.settings.classNames.form}" novalidate>
-                ${Object.keys(model.settings.types).map(type => `<fieldset class="${model.settings.classNames.fieldset}">
-                <legend class="${model.settings.classNames.legend}">
-                    <span class="${model.settings.classNames.title}">${model.settings.types[type].title}</span>
-                    <span class="${model.settings.classNames.description}">${model.settings.types[type].description}</span>
-                </legend>
-                <div class="form-row">
-                    <div class="relative">
-                        <label class="privacy-banner__label">
-                            <input
-                                class="${model.settings.classNames.field}"
-                                type="radio"
-                                name="privacy-${type.split(' ')[0].replace(' ', '-')}"
-                                value="1"
-                                ${model.consent[type] === 1 ? ` checked` : ''}>
-                            <span class="privacy-banner__label-text">I am OK with this</span>
-                            <span class="privacy-banner__label-description">${model.settings.types[type].labels.yes}</span>
-                        </label>    
-                    </div>
-                </div>
-                <div class="form-row">
-                    <div class="relative">
-                        <label class="privacy-banner__label">
-                            <input
-                                class="${model.settings.classNames.field}"
-                                type="radio"
-                                name="privacy-${type.split(' ')[0].replace(' ', '-')}"
-                                value="0"
-                                ${model.consent[type] === 0 ? ` checked` : ''}>
-                            <span class="privacy-banner__label-text">No thank you</span>
-                            <span class="privacy-banner__label-description">${model.settings.types[type].labels.no}</span>
-                        </label>    
-                    </div>
-                </div>
-            </fieldset>`).join('')}
-            <button class="${model.settings.classNames.submitBtn}"${Object.keys(model.consent).length === 0 ? ` disabled` : ''}>Save my settings</button>
-        </form>`;
-    }
-}
-
-

Utility functions

-

There are two utility functions provided by the library designed to be invoked following user consent.

-

Render iframe

-

state.utils.renderIframe

-

Renders an iframe from a placeholder element with specific data attributes:

-
<div data-iframe-src="https://www.youtube.com/embed/qpLKTUQev30" data-iframe-title="Test video" data-iframe-height="1600px" data-iframe-width="900px">
-    <p>Update your cookie preferences to view this content</p>
-    <button class="js-preferences-update">Update</button>
-</div>
-
-

In the cookie banner configuration:

-
import cookieBanner from '@stormid/cookie-banner';
-
-cookieBanner({
-    ...lots of other config
-    type: {
-        thirdParty: [
-            state => state.utils.renderIframe()
-        ]
-    }
-})
-
-

Google Tag Manager Snippet

-

state.utils.gtmSnippet

-

Invokes a GTM snippet to load the GTM library via an script element, just pass the Tag Manager ID/UA number as an argument

-

In the cookie banner configuration:

-
import cookieBanner from '@stormid/cookie-banner';
-
-cookieBanner({
-    ...lots of other config
-    type: {
-        thirdParty: [
-            state => state.utils.gtmSnippet(`UA-1234-5678`)
-        ]
-    }
-})
-
-

API

-

The Object returned from initialisation exposes the interface

-
{
-    getState, Function that returns the current state Object
-    showBanner, Function to show the banner, accepts a callback function
-    renderForm, Function to render the consent form
-}
-
-

Events

-

There are three custom events that an instance of the cookie banner dispatches:

- -

The events are dispatched on the document. A reference to the getState function of the instance is contained in the custom event detail.

-
const instance = banner(options);
-
-document.addEventListener('banner.show', e => {
-    //e.g. initialise toggle for form-in-banner implementation
-    const [ bannerToggle ] = toggle('.js-banner-toggle'); 
-    const state = e.detail.getState();
-    // do something with state if we want to
-});
-
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index a945ed06..00000000 --- a/docs/index.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - StormId Components - boilerplate - - - - -
- - -
-
- - - -

Component Name

-

This is a boilerplate for developing UI components by Storm Id.

-
-

Usage

-

Create a container element in HTML

-
<div class="js-boilerplate"></div>
-
-

Install the package

-
npm i -S @stormid/boilerplate
-
-

Import the module

-
import boilerplate from '@stormid/boilerplate';
-
-Initialise the module via selector string
-
-

const [ instance ] = boilerplate(’.js-boilerplate’);

-

-Initialise with a DOM element
-
-

const element = document.querySelector(’.js-boilerplate’); -const [ instance ] = boilerplate(element);

-

-Initialise with a Node list
-
-

const elements = document.querySelectorAll(’.js-boilerplate’); -const [ instance ] = boilerplate(elements);

-

-Initialise with an Array of elements
-
-

const elements = [].slice.call(document.querySelectorAll(’.js-boilerplate’)); -const [ instance ] = boilerplate(elements);

-

-## Options
-
-

{ -callback: null -}

-

-For example
-
-

boilerplate(’.js-selector’, { -callback(){ -console.log(this); -} -});

-

-## API
-boilerplate() returns an array of instances. Each instance exposes the interface
-
-

{ -node, DOMNode augmented by initialisation -click, trigger the handleClick method -}

-

-## Tests
-
-

npm t

-

-## Browser support
-
-## Dependencies
-
-## License
-MIT
- -
- - \ No newline at end of file diff --git a/docs/modal-gallery.html b/docs/modal-gallery.html deleted file mode 100644 index db762bf0..00000000 --- a/docs/modal-gallery.html +++ /dev/null @@ -1,339 +0,0 @@ - - - - StormId Components - modal-gallery - - - - -
- - -
-
- - - -

Modal Gallery

-

Accessible modal image gallery

-
-

Usage

-

Install the package

-
npm i -S @stormid/modal-gallery
-
-

A modal gallery can be created with DOM elements, or programmatically created from a JS Object.

-

From HTML

-
<ul>
-    <li>
-        <a class="js-modal-gallery" href="https://placehold.co/500x500" data-title="Image 1" data-description="Description 1" data-srcset="https://placehold.co/800x800 800w, https://placehold.co/500x500 320w">Image one</a>
-    </li>
-    <li>
-        <a class="js-modal-gallery" href="https://placehold.co/300x800" data-title="Image 2" data-description="Description 2" data-srcset="https://placehold.co/500x800 800w, https://placehold.co/300x500 320w">Image two</a
-    ></li>
-</ul>
-
-

Initialise the module

-
import modalGallery from '@stormid/modal-gallery';
-
-const [ gallery ] = modalGallery('.js-modal-gallery');
-
-

Example MVP CSS

-
.modal-gallery__outer {
-    display: none;
-    opacity: 0;
-    position: fixed;
-    overflow: hidden;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    z-index: 100;
-    background-color: rgba(0,0,0,.9);
-    transition: opacity 500ms ease;
-}
-.modal-gallery__outer.is--active {
-    display: block;
-    opacity: 1;
-}
-.modal-gallery__img-container {
-    text-align:center;
-}
-.modal-gallery__img {
-    margin:80px auto 0 auto;
-    max-width:80%;
-    max-height: 80vh;
-}
-.modal-gallery__item {
-    position: fixed;
-    top:0;
-    left:0;
-    right:0;
-    bottom:0;
-    opacity:0;
-    visibility:hidden;
-}
-.modal-gallery__item.is--active {
-    opacity:1;
-    visibility:visible;
-}
-.modal-gallery__next {
-    position: fixed;
-    bottom:50%;
-    right:25px;
-}
-.modal-gallery__previous {
-    position: fixed;
-    bottom:50%;
-    left:25px;
-}
-.modal-gallery__close {
-    position: fixed;
-    top:15px;
-    right:25px;
-}
-.modal-gallery__close:hover svg,
-.modal-gallery__previous:hover svg,
-.modal-gallery__next:hover svg{
-    opacity:.8
-}
-.modal-gallery__total {
-    position: absolute;
-    bottom:25px;
-    right:25px;
-    color:#fff
-}
-.modal-gallery__details {
-    position: fixed;
-    bottom:0;
-    left:120px;
-    right:120px;
-    padding:0 0 40px 0;
-    color:#fff;
-}
-
-

To create from a JavaScript Object

-
import modalGallery from '@stormid/modal-gallery';
-
-const [ gallery ] = modalGallery([
-    {
-        src: 'https://placehold.co/500x500',
-        srcset:'https://placehold.co/800x800 800w, https://placehold.co/500x500 320w',
-        title: 'Image 1',
-        description: 'Description 1'
-    },
-    {
-        src: 'https://placehold.co/300x800',
-        srcset:'https://placehold.co/500x800 800w, https://placehold.co/300x500 320w',
-        title: 'Image 2',
-        description: 'Description 2'
-    }
-]);
-
-//e.g. Open the gallery at the second item (index 1) by clicking on a button with the className 'js-modal-gallery__trigger'
-document.querySelector('.js-modal-gallery__trigger').addEventListener('click', () => gallery.open(1));
-
-

Options

-
{
-    fullscreen: false, //show gallery in fullscreen
-    preload: false, //preload all images
-    totals: true,   //show totals
-    scrollable: false, //modal is scrollable
-    single: false, //single image or gallery
-}
-
-

API

-

modalGallery() returns an array of instances. Each instance exposes the interface

-
{
-    getState, a Function that returns the current state Object
-    open, a Function that opens the modal gallery
-}
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/modal.html b/docs/modal.html deleted file mode 100644 index 110796e8..00000000 --- a/docs/modal.html +++ /dev/null @@ -1,269 +0,0 @@ - - - - StormId Components - modal - - - - -
- - -
-
- - - -

Modal

-

Accessible modal dialog

-
-

Usage

-

Create a modal dialog and button(s) to toggle in HTML

-
<button class="js-modal-toggle">Open modal</button>
-<div id="modal-1" class="js-modal modal" data-modal-toggle="js-modal-toggle" hidden>
-    <div class="modal__inner" role="dialog" aria-labelledby="modal-label">
-        <h2 id="modal-label">Modal title</h2>
-        ...
-        <button class="modal__close-btn js-modal-toggle" aria-label="close">
-            <svg focusable="false" fill="#fff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
-                <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
-                <path d="M0 0h24v24H0z" fill="none"/>
-            </svg>
-        </button>
-    </div>
-</div>
-
-

Install the package

-
npm i -S @stormid/modal
-
-

Import the module

-
import modal from '@stormid/modal';
-
-

Initialise the module via selector string

-
const [ instance ] = modal('.js-modal');
-
-

Initialise with a DOM element

-
const element = document.querySelector('.js-modal');
-const [ instance ] = modal(element);
-
-

Initialise with a Node list

-
const elements = document.querySelectorAll('.js-modal');
-const [ instance ] = modal(elements);
-
-

Initialise with an Array of elements

-
const elements = [].slice.call(document.querySelectorAll('.js-modal'));
-const [ instance ] = modal(elements);
-
-

CSS -The className ‘is–modal’ added to the document.body when the modal is open. This can be used to prevent the body from scrolling

-
.is--modal {
-    overflow: hidden;
-}
-
-

Options

-

Options can be set during initialising in an Object passed as the second argument to the modal function, e.g. modal('.js-modal', { startOpen: true }), or as data-attributes on the modal element (the element passed to the modal function), e.g. data-start-open="true"

-
{
-    onClassName: 'is--active', //className added to node when modal is open
-    toggleSelectorAttribute: 'data-modal-toggle', //attribute on node to use as toggle selector
-    callback: false, //optional function called after modal state change
-    delay: 0, //ms delay before focus on first focuable element
-    startOpen //boolean, to trigger modal to open when initialised    
-}
-
-

API

-

modal() returns an array of instances. Each instance exposes the interface

-
{
-    getState, a Function that returns the current state Object
-    open, a Function that opens the modal
-    close a Function that closes the modal
-}
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/outliner.html b/docs/outliner.html deleted file mode 100644 index bbbfd214..00000000 --- a/docs/outliner.html +++ /dev/null @@ -1,222 +0,0 @@ - - - - StormId Components - outliner - - - - -
- - -
-
- - - -

Outliner

-

Adds a classNamw to the documentElement to be used to hide CSS outline on mouse interactions, show on keyboard interactions. Until :focus-visible has broader browser support.

-
-

Usage

-

Install the package

-
npm i -S @stormid/outliner
-
-

Initialise the module

-
import '@stormid/outliner';
-
-
-

Add CSS

-
.no-outline * {
-    outline: 0 none !important;
-    box-shadow: none !important;
-}
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/scroll-points.html b/docs/scroll-points.html deleted file mode 100644 index 1729a48e..00000000 --- a/docs/scroll-points.html +++ /dev/null @@ -1,245 +0,0 @@ - - - - StormId Components - scroll-points - - - - -
- - -
-
- - - -

Scroll points

-

Trigger className changes and callbacks based on element intersecting the viewport using IntersectionObservers.

-
-

Usage

-

Add the selector to the DOMElement you wish to become a scroll-point

-
<div class="js-scroll-point"></div>
-
-

Install the package

-
npm i -S @stormid/scroll-points
-
-

Import the module

-
import scrollPoints from '@stormid/scroll-points';
-
-

Initialise the module via selector string

-
const [ instance ] = scrollPoints('.js-scroll-points');
-
-

Initialise with a DOM element

-
const element = document.querySelector('.js-scroll-points');
-const [ instance ] = scrollPoints(element);
-
-

Initialise with a Node list

-
const elements = document.querySelectorAll('.js-scroll-points');
-const [ instance ] = scrollPoints(elements);
-
-

Initialise with an Array of elements

-
const elements = [].slice.call(document.querySelectorAll('.js-scroll-points'));
-const [ instance ] = scrollPoints(elements);
-
-

Options

-
{
-	root: null, //element that is used as the viewport for checking visiblity of the target
-	rootMargin: '0px 0px 0px 0px', //margin around the root, px or percentage values
-	threshold: 0, //Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed
-	callback: false, //function executed when scrolled into view
-	className: 'is--scrolled-in', //className added when scrolled into view
-	unload: true //only callback once
-};
-
-

Tests

-
npm t
-
-

Browser support

-

Depends on Object.assign and the IntersectionObserver API, IE11 will require polyfills.

-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/scroll-spy.html b/docs/scroll-spy.html deleted file mode 100644 index cce5b800..00000000 --- a/docs/scroll-spy.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - StormId Components - scroll-spy - - - - -
- - -
-
- - - -

Scroll Spy

-

Use the IntersectionObserver API to check when a section of the document is in view and update an associated DOM node.

-

Useful for scroll position-related navigation state management.

-
-

Example usage

-
<header>
-    <nav aria-label="mMain navigation">
-        <a class="js-scroll-spy" href="#section1">Section 1</a>
-        <a class="js-scroll-spy" href="#section2">Section 2</a>
-        <a class="js-scroll-spy" href="#section3">Section 3</a>
-    </nav>
-</header>
-<main>
-    <section id="section1" aria-label="Section 1">
-    ...
-    </section>
-    <section id="section2" aria-label="Section 2">
-    ...
-    </section>
-    <section id="section3" aria-label="Section 3">
-    ...
-    </section>
-</main>
-
-

Install the package

-
npm i -S @stormid/scroll-spy
-
-

Import the module

-
import scrollSpy from '@stormid/scroll-spy';
-
-

Initialise the module via selector string

-
const instance = scrollSpy('.js-scroll-spy');
-
-

Initialise with a DOM element

-
const element = document.querySelector('.js-scroll-spy');
-const instance = scrollSpy(element);
-
-

Initialise with a Node list

-
const elements = document.querySelectorAll('.js-scroll-spy');
-const [ instance ] = scrollSpy(elements);
-
-

Initialise with an Array of elements

-
const elements = [].slice.call(document.querySelectorAll('.js-scroll-spy'));
-const instance = scrollSpy(elements);
-
-

Options

-
{
-	root: null, //element that is used as the viewport for checking visiblity of the target
-	rootMargin: '0px 0px 0px 0px', //margin around the root, px or percentage values
-	threshold: 0, //Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed
-    activeClassName: 'is--active', //className added when in view
-	callback: null, //function executed when intersecting view
-	single: true // boolean to indicate whether a single or multiple spies can be active at once
-}
-
-

API

-

scrollSpy() returns an array of instances. Each instance exposes the interface

-
{
-    getState, a Function that returns the current state Object
-}
-
-

Tests

-
npm t
-
-

Browser support

-

Depends on Object.assign and the IntersectionObserver API, IE11 will require polyfills.

-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/skip.html b/docs/skip.html deleted file mode 100644 index 240e3704..00000000 --- a/docs/skip.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - StormId Components - skip - - - - -
- - -
-
- - - -

Skip

-

Ensure fragment identifier links (e.g. ‘skip to content’) focus on their target node

-
-

Usage

-

JS

-
npm i -S @stormid/skip 
-
-
import '@stormid/skip';
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/tabs.html b/docs/tabs.html deleted file mode 100644 index 65fcc80e..00000000 --- a/docs/tabs.html +++ /dev/null @@ -1,267 +0,0 @@ - - - - StormId Components - tabs - - - - -
- - -
-
- - - -

Tabs

-

Accessible tabbed panelled content areas

-
-

Usage

-

Create a tablist in HTML

-
<div class="tabs js-tabs">
-    <div class="tabs__tabslist" role="tablist">
-        <a id="tab-1" class="tabs__tab js-tabs__link" href="#panel-1" role="tab">Tab 1</a>
-        <a id="tab-2" class="tabs__tab js-tabs__link" href="#panel-2" role="tab">Tab 2</a>
-        <a id="tab-3" class="tabs__tab js-tabs__link" href="#panel-3" role="tab">Tab 3</a>
-    </div>
-    <div id="panel-1" class="tabs__tabpanel" role="tabpanel">Panel 1</div>
-    <div id="panel-2" class="tabs__tabpanel" role="tabpanel" hidden>Panel 2</div>
-    <div id="panel-3" class="tabs__tabpanel" role="tabpanel" hidden>Panel 3</div>
-</div>
-
-

Install the package

-
npm i -S @stormid/tabs
-
-

Import the module

-
import tabs from '@stormid/tabs';
-
-

Initialise the module via selector string

-
const [ instance ] = tabs('.js-tabs');
-
-

Initialise with a DOM element

-
const element = document.querySelector('.js-tabs');
-const [ instance ] = tabs(element);
-
-

Initialise with a Node list

-
const elements = document.querySelectorAll('.js-tabs');
-const [ instance ] = tabs(elements);
-
-

Initialise with an Array of elements

-
const elements = [].slice.call(document.querySelectorAll('.js-tabs'));
-const [ instance ] = tabs(elements);
-
-

Options

-
{
-    tabSelector: '[role=tab]', // selector for a tab link  
-    activeClass: 'is--active', //className added to active tab
-    updateURL: true, //push tab fragment identifier to window location hash
-    activeIndex: 0 //index of initially active tab
-    focusOnLoad: true //a boolean to set whether the page should focus on the first tab after loading
-}
-
-

Setting the active tab

-
On page load the active tab will be set by (in order of precedence):
-1. The page hash.  If the page hash in the address bar matches the ID of a panel, it will be activated on page load
-2. The data-active-index attribute.  If the tabs node found to have a <pre>data-active-index</pre> attribute, that tab will be activated on page load.  This is a zero-based index.   
-3. The tab specified by the activeIndex in the settings. This is a zero-based index.
-4. The first tab in the set.
-
-## API
-
-tabs() returns an array of instances. Each instance exposes the interface
-
-

{ -getState, a Function that returns the current state Object -}

-

-## Tests
-
-

npm t

-

-## License
-MIT
- -
- - \ No newline at end of file diff --git a/docs/textarea.html b/docs/textarea.html deleted file mode 100644 index a61a3fa4..00000000 --- a/docs/textarea.html +++ /dev/null @@ -1,244 +0,0 @@ - - - - StormId Components - textarea - - - - -
- - -
-
- - - -

Textarea

-

Auto-resizing textarea

-
-

Usage

-

Install the package

-
npm i -S @stormid/textarea
-
-

Import the module

-
import textarea from '@stormid/textarea';
-
-

Initialise the module via selector string

-
const [ instance ] = boilerplate('.js-boilerplate');
-
-

Initialise with a DOM element

-
const element = document.querySelector('textarea');
-const [ instance ] = textarea(element);
-
-

Initialise with a Node list

-
const elements = document.querySelectorAll('textarea');
-const [ instance ] = textarea(elements);
-
-

Initialise with an Array of elements

-
const elements = [].slice.call(document.querySelectorAll('textarea'));
-const [ instance ] = textarea(elements);
-
-

Options

-
{
-    events: [
-        'input' //default textarea resize event
-    ]
-}
-
-

API

-

textarea() returns an array of instances. Each instance exposes the interface

-
{
-    node, DOMElement, the text area
-    resize, Function to trigger resize
-}
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/toggle.html b/docs/toggle.html deleted file mode 100644 index f819a502..00000000 --- a/docs/toggle.html +++ /dev/null @@ -1,310 +0,0 @@ - - - - StormId Components - toggle - - - - -
- - -
-
- - - -

Toggle

-

Accessible DOM state toggling for off-canvas and show/hide UI patterns using aria-expanded.

-
-

Usage

-

For page-level state toggling (e.g. an off-canvas menu)

-

Create a target and related button(s) in HTML

-
<button class="js-toggle-btn">Menu</button>
-<nav id="primary-navigation" aria-label="Main navigation" class="js-toggle" data-toggle="js-toggle-btn">...</nav>
-
-

Install the package

-
npm i -S @stormid/toggle
-
-

Import the module

-
import toggle from '@stormid/toggle';
-
-

Initialise the module via selector string

-
const [ instance ] = toggle('.js-toggle');
-
-

Initialise with a DOM element

-
const element = document.querySelector('.js-toggle');
-const [ instance ] = toggle(element);
-
-

Initialise with a Node list

-
const elements = document.querySelectorAll('.js-toggle');
-const [ instance ] = toggle(elements);
-
-

Initialise with an Array of elements

-
const elements = [].slice.call(document.querySelectorAll('.js-toggle'));
-const [ instance ] = toggle(elements);
-
-

Local toggle

-

To localise a toggle state to part of the document (e.g. show/hide panel)

-

Create a target and related button(s) in HTML

-
<div class="parent">
-    <button type="button" class="js-toggle__btn"></button>
-    <div id="child" class="js-toggle__local child" data-toggle="js-toggle__btn"></div>
-</div>
-
-

Example MVP CSS

-
.child {
-    display: none
-}
-.parent.is--active .child {
-    display: static;
-}
-
-

Options

-
{
-    delay: 0, //duration of animating out of toggled state
-    startOpen: false,  //initial toggle state
-    local: false, // encapsulate in small part of document
-    prehook: false, //function to fire before each toggle
-    callback: false, //function to fire after each toggle
-    focus: false, //focus on first focusable child node of the target element
-    trapTab: false, //trap tab in the target element
-    closeOnBlur: false, //close the target node on losing focus from the target node and any of the toggles
-    closeOnClick: false, //close the target element when a non-child element is clicked
-}
-
-

e.g.

-
const [ instance ] = toggle('.js-toggle', {
-    startOpen: true
-});
-
-

Options can also be set on an instance by adding data-attributes to the toggle element, e.g.

-
<div class="parent">
-    <button type="button" class="js-toggle__btn"></button>
-    <div class="js-toggle__local" data-toggle="js-toggle__btn" data-start-open="true"></div>
-</div>
-
-

A toggle can also be started open usng the active className alone, e.g.

-
<div class="parent is--active">
-    <button type="button" class="js-toggle__btn"></button>
-    <div class="js-toggle__local" data-toggle="js-toggle__btn"></div>
-</div>
-
-

Developer note 06 Sep 2022: Use of closeOnBlur

-

It should be noted that at the time of writing, the availaibility of the blur event was limited on mobile assistive tech, specifically iOS VoiceOver.

-

When a user is swiping through content in VoiceOver, the focus/blur events will only fire if the focus is moving to or from a form input element or button. The focus/blur events will not fire when moving between links, headings or in-page content. Any use of the closeOnBlur setting should be carefully tested to make sure that the behaviour is as expected on these devices.

-

API

-

toggle() returns an array of instances. Each instance exposes the interface

-
{
-    node, DOMElement, the text area
-    startToggle, a Function that starts the toggle lifecycle with prehook, toggle, and post-toggle callback
-    toggle, a Function that just executes the toggle
-    getState, a Function that returns the current state Object
-}
-
-

Events

-

There are two custom events that an instance of the toggle dispatches:

-
    -
  • toggle.open when it opens
  • -
  • toggle.close when closes
  • -
-

The events are dispatched on the same element used to initialise the toggle and bubble for event delegation. The a reference to the getState function of the instance is contained in the custom event detail.

-
const [ instance ] = toggle('.js-toggle');
-
-//event bubbles so can delegate
-//could also add event listener to document.querySelector('.js-toggle')
-document.addEventListener('toggle.open', e => {
-  const { node, toggles } = e.detail.getState();
-  // do something
-});
-
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/docs/validate.html b/docs/validate.html deleted file mode 100644 index e0f13b14..00000000 --- a/docs/validate.html +++ /dev/null @@ -1,563 +0,0 @@ - - - - StormId Components - validate - - - - -
- - -
-
- - - -

Validate

-

Client-side form validation library to support .NET validation using data-val attributes as a drop-in replacement for jQuery validate, and HTML5 attribute-based constraint validation.

-
-

Contents

- -

Usage

-

Install the package

-
npm i -S @stormid/validate
-
-

Import the module

-
import validate from '@stormid/validate';
-
-

Initialise the module via selector string

-
const [ validator ] = validate('form:not([novalidate])');
-
-

Initialise with a DOM element

-
const element = document.querySelector('form:not([novalidate])');
-const [ validator ] = validate(element);
-
-

Initialise with a Node list

-
const elements = document.querySelectorAll('form:not([novalidate])');
-const [ validator ] = validate(elements);
-
-

Initialise with an Array of elements

-
const elements = [].slice.call(document.querySelectorAll('form:not([novalidate])'));
-const [ validator ] = validate(elements);
-
-

Validators

-

This library supports HTML5 attribute constraints and the data-val attributes generated by .Net Core model validation, .Net MVC DataAnnotation, or .Net Fluent validation libraries.

-

Multiple validators can be used on a single field. Custom validators can be added via the addMethod API.

- -
-

Required

-

The field must have a value. Checkboxes are treated as non-nullable in required validation.

-

HTML5

-
<input name="field" id="field" required>
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-required="'field' is required">
-
-
-

Email

-

Value is matched against the regular expression

-
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
-
-

which is equivalent to the browser algorithm used in the constraint validation API for type=“email”, and is intentionally loose due to known issues related to international domain names and the validation of e-mail addresses in HTML. Stricter or more specific validation rules can be specified with a pattern attribute (or data-val-regex).

-

HTML5

-
<input type="email" name="field" id="field">
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-email="'field' must be a valid email address">
-
-
-

Url

-

The value is matched against this regular expresion by Diego Perini

-
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
-
-

Stricter or more specific validation rules can be specified with a pattern attribute (or data-val-regex).

-

HTML5

-
<input type="url" name="field" id="field">
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-url="'field' must be a valid url">
-
-
-

Pattern/Regex

-

Value matches the supplied pattern or regular expression

-

HTML5

-
<input name="field" id="field" pattern="^http(s)?">
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-regex="'field' must start with http or https" data-val-regex-pattern="^http(s)?">
-
-
-

Digits

-

Value must contain only characters in the range 0-9

-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-digits="'field' must be a number">
-
-
-

Number

-

Value is matched against the regular expresion

-
/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
-
-

Allowing positive and negative numbers, decimals, and comma separated thousands.

-

HTML5

-
<input type="number" name="field" id="field">
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-number="'field' must be a number">
-
-
-

Min

-

Value is a Number greater or equal to min

-

HTML5

-
<input type="number" name="field" id="field" min="0">
-
-

Data attributes

-
<input type="number" name="field" id="field" data-val="true" data-val-min="'field' must be >= 0" data-val-min-min="0">
-
-
-

Max

-

Value is a Number less than or equal to max

-

HTML5

-
<input type="number" name="field" id="field" max="100">
-
-

Data attributes

-
<input type="number" name="field" id="field" data-val="true" data-val-max="'field' must be <= 100" data-val-max-max="100">
-
-
-

Range

-

Value is a Number within the specified min and max

-

Data attributes

-
<input type="number" name="field" id="field" data-val="true" data-val-range="'field' must be between 0 and 100" data-val-range-min="0" data-val-range-max="100">
-
-
-

Length

-

Value is a String with a length greater than or equal to min and/or a length less than or equal to max

-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-length="'field' length must be between 2 and 4" data-val-length-min="2" data-val-length-max="4">
-
-
-

Stringlength

-

Value is a String with a length less than or equal to max. Equivalent to the length/max validator, generated by .Net stringlength data notation.

-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-stringlength="'field' length must be less than 4" data-val-stringlength-max="4">
-
-
-

Maxlength

-

Value is a String with a length less than or equal to max. Equivalent to the length/max validator, generated by .Net maxlength data notation.

-

.HTML5

-
<input name="field" id="field" maxlength="4">
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-maxlength="'field' length must be less than 4" data-val-maxlength-max="4">
-
-
-

Minlength

-

Value is a String with a length greater than or equal to min. Equivalent to the length/min validator, generated by .Net minlength data notation.

-

.HTML5

-
<input name="field" id="field" minlength="2">
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-minlength="'field' length must be greater than 2" data-val-minlength-min="2">
-
-
-

DateISO

-

Value is a string in a format matching the date ISO standard, (YYYY-MM-DD), matching the regular expression

-
/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/
-
-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-dateISO="'field' must in the format YYYY-MM-DD">
-
-
-

Equalto

-

Value must match the value of another field (or multiple fields). The data-val-equalto-other attribute is the name of another field, or a comma-separated list of names of multiple fields.

-

Data attributes

-
<input name="password" id="password">
-...
-<input name="passwordConfirmation" id="passwordConfirmation" data-val="true" data-val-equalto="'password confirmation' must match 'password'" data-val-equalto-other="password">
-
-
-

Remote

-

Value is validated against via XHR against a remote resource. The resouce is defined by the data-val-remote-url attribute.

-

Defaults to a POST request, data-val-remote-type can be set to make GET requests.

-

The values of additional fields can be sent in the request by specifying a data-val-remote-additionalfields attribute, a comma separated list of field ids to be included.

-

Data attributes

-
<input name="field" id="field" data-val="true" data-val-remote="'field' must pass remote validation" data-val-remote-url="/api/validate">
-
-
-

Errors

-

Error message container

-

The element to contain server-side error messages generated by .Net fluent and unobtrustive validation libraries is recycled by this library, it can also be manually added to HTML when you need more control over the position and markup of the error message container. The data-valmsg-for provides the association between error message and field/field group name, the id is used by the library to associate the error with the input using aria-describedby.

-
<span id="field-error-message" class="field-validation-valid" data-valmsg-for="field" />
-
-

If this element is not present a span is appended to the label for the field with the className .error-message.

-

Error messages

-

.Net error messages are extracted from data-val-[validator-type] data attributes, and apply to both HTML5 and .Net validators.

-

Fields without data-val error messages will show the default messages for the failed validator (see options below).

-

Including values in error messages

-

To include the user input value in your error message, place a token “{{value}}” within the message string and the script will replace it at the time of validation. e.g. “{{value}} is not a valid email address” will become “test@test is not a valid email address”.

-

If a validation group contains more than one field, the values of these will be returned as a comma seperated list within the message. For example: “{{value}} are not valid inputs” becomes “test1, test2 are not valid inputs”.

-

Options

-
{
-    preSubmitHook: false, //function, called on validation pass, before submit
-    submit: form.submit, // function, to support async form submissions, pass your own submit function
-    messages: { //default HTML5 error messages
-        required() { return 'This field is required'; } ,
-        email() { return 'Please enter a valid email address'; },
-        pattern() { return 'The value must match the pattern'; },
-        url(){ return 'Please enter a valid URL'; },
-        number() { return 'Please enter a valid number'; },
-        digits() { return 'Please enter only digits'; },
-        maxlength(props) { return `Please enter no more than ${props.max} characters`; },
-        minlength(props) { return `Please enter at least ${props.min} characters`; },
-        max(props){ return `Please enter a value less than or equal to ${props.max}`; },
-        min(props){ return `Please enter a value greater than or equal to ${props.min}`}
-    }
-}
-
-

API

-

validate() returns an array of instances. Each instance exposes the interface

-
{
-    validate
-    addMethod
-    validateGroup
-    addGroup
-    removeGroup
-}
-
-

addMethod

-

Add a custom validation method to a group:

-
const [ validator ] = validate('.my-form');
-
-validator.addMethod(
-    'MyFieldName', //input/input group name/or validation group name if passing an array of fields
-    (value, fields) => { //validation method
-        //value is the value of the whole group of fields (grouped under the name attribute)
-        return value === 'test'; //must return boolean
-    },
-    'Value must equal "test"', //error message on validation failure
-    fields // an optional array of inputs, if this isn't present the input/groupName is used as a name (or data-group) attribute selector
-);
-
-

validate

-

Manually trigger validation on the whole form, returns a promise:

-
const [ validator ] = validate('.my-form');
-
-await validator.validate();
-
-

addGroup

-

Add a field or field group to the validator:

-
const [ validator ] = validate('.my-form');
-const fieldsArray = Array.from(document.querySelector('.new-fields'))
-
-//add by passing an array of fields
-//if these fields span multiple groups they will be collected into the correct validation groups internally by the validator
-validator.addGroup(fieldsArray);
-
-

validateGroup

-

Immediately validates an individual group within the form:

-
const [ validator ] = validate('.my-form');
-const validator.validateGroup('myInput');
-
-//pass in the name or data-val-group value that corresponds to the group you're looking to validate
-//returns a promise which resolves with the validity state of the group (true if valid, false if invalid)
-
-

removeGroup

-

Remove a validation group from the validator:

-
const [ validator ] = validate('.my-form');
-const fieldsArray = Array.from(document.querySelectorAll([name=new-fields]))
-
-//add by passing an array of fields
-validator.addGroup(fieldsArray);
-
-//remove by passing the name of a group
-validator.removeGroup('new-fields');
-
-

Plugins

-

Plugins are a set of pre-built custom validators that are included in the package but not in the default build. They can be imported and used in addMethod you would your own custom validation method.

-

isValidDate

-

Validate three separate day/month/year fields (similar to the govuk design system date component) as a single valid date.

-

The minimum accepted year value in the isValidDate plugin is 1000. To set a different (more recent) minimum value consider using the min validator on the year input.

-

isFutureDate

-

Validate three separate day/month/year fields (similar to the govuk design system date component) as a single date in the future.

-

isPastDate

-

Validate three separate day/month/year fields (similar to the govuk design system date component) as a single date in the past- today’s date is valid.

-

HTML

-
<fieldset>
-    <legend>
-        <span>Date</span>
-        <span data-valmsg-for="date" id="date-error-message"></span>
-        <span data-valmsg-for="dateDay" id="date-Day-error-message"></span>
-        <span data-valmsg-for="dateMonth" id="date-Month-error-message"></span>
-        <span data-valmsg-for="dateYear" id="date-Year-error-message"></span>
-    </legend>
-    <div class="flex">
-        <input id="dateDay" name="dateDay" inputmode="numeric" data-val="true" data-val-required="Enter a day" aria-required="true"/>
-        <input id="dateMonth" name="dateMonth" inputmode="numeric" data-val="true" data-val-required="Enter a month" aria-required="true"/>
-        <input id="dateYear" name="dateYear" inputmode="numeric" data-val="true" data-val-required="Enter a year" aria-required="true" />
-    </div>
-</fieldset>
-
-

JS

-
import validate from '@stormid/validate';
-import { isValidDate } from '@stormid/validate/src/lib/plugins/methods/date';
-
-const [ validator ] = validate('.my-form');
-validator.addMethod(
-  'date', //name of custom validation group
-  isValidDate, // date validation method imported from the library 
-  'Enter a valid date', // error message
-  [ document.getElementById('dateDay'), document.getElementById('dateMonth'), document.getElementById('dateYear') ] //date fields array [day, month, year]
-);
-
-

Tests

-
npm t
-
-

License

-

MIT

- -
- - \ No newline at end of file diff --git a/packages/toggle/.npmignore b/packages/toggle/.npmignore index 97d59ca6..e9c7665c 100644 --- a/packages/toggle/.npmignore +++ b/packages/toggle/.npmignore @@ -1,6 +1,5 @@ .DS_Store *.log -src __tests__ example coverage diff --git a/packages/toggle/README.md b/packages/toggle/README.md index 4aaa1963..d4bbd692 100644 --- a/packages/toggle/README.md +++ b/packages/toggle/README.md @@ -1,69 +1,99 @@ # Toggle -Accessible DOM state toggling for off-canvas and show/hide UI patterns using aria-expanded. +Accessible DOM state toggling utility to support the expansion and collapse of regions of HTML documents using aria-expanded. Useful for expandable sections and off-canvas navigation patterns. + +For well-tested implementations of UI patterns using Toggle look at https://storm-ui-patterns.netlify.app. --- ## Usage -For page-level state toggling (e.g. an off-canvas menu) +For well-tested implementations of UI patterns using Toggle look at https://storm-ui-patterns.netlify.app. + +To install +``` +npm i -S @stormid/toggle +``` -Create a target and related button(s) in HTML +### Full document toggle +Useful for document-level state changes that affect the whole page, such as an off-canvas menu. + +1. Set up the DOM elements +The element you want to toggle state, and related button(s) that will trigger state change. The `data-toggle` attribute of the target element (nav in the example below) is used as a selector to find buttons that trigger state change. The target element should have a unique id. + +Simplified example: ``` ``` -Install the package +1. Set up CSS +Toggle changes DOM attributes and CSS classNames but all visible changes to the UI are left to the developer to implement in CSS. + +For a full document Toggle a className is added to the document element (html) based on the target id - "on--" plus the target id. + +Simplified example: ``` -npm i -S @stormid/toggle +.nav { + display: none; +} +.on--primary-navigation .nav { + display: block; +} ``` -Import the module +3. Set up JavaScript +Install ``` -import toggle from '@stormid/toggle'; +npm i -S @stormid/toggle ``` -Initialise the module via selector string +Implement ``` +import toggle from '@stormid/toggle'; + const [ instance ] = toggle('.js-toggle'); ``` +In addition to a CSS selector, Toggle also supports initialisation via -Initialise with a DOM element +DOM element ``` const element = document.querySelector('.js-toggle'); const [ instance ] = toggle(element); ``` -Initialise with a Node list +Node list ``` const elements = document.querySelectorAll('.js-toggle'); const [ instance ] = toggle(elements); ``` - -Initialise with an Array of elements +Array of elements ``` const elements = [].slice.call(document.querySelectorAll('.js-toggle')); const [ instance ] = toggle(elements); ``` -### Local toggle -To localise a toggle state to part of the document (e.g. show/hide panel) +### Localised toggle +Useful for localised state changes affecting a smaller part of the document, such as an exapandable section. -Create a target and related button(s) in HTML +1. Set up the DOM +Simplified example ``` -
+ ``` -Example MVP CSS +2. Set up CSS +A className ('is--active') is added to the parentNode of the target in a localised toggle. + +Simplified example ``` .child { display: none } .parent.is--active .child { - display: static; + display: block; } ``` @@ -75,10 +105,11 @@ Example MVP CSS local: false, // encapsulate in small part of document prehook: false, //function to fire before each toggle callback: false, //function to fire after each toggle - focus: false, //focus on first focusable child node of the target element + focus: true, //focus on first focusable child node of the target element trapTab: false, //trap tab in the target element closeOnBlur: false, //close the target node on losing focus from the target node and any of the toggles closeOnClick: false, //close the target element when a non-child element is clicked + useHidden: false //add and remove hidden attribute to toggle target } ``` e.g. @@ -111,10 +142,10 @@ When a user is swiping through content in VoiceOver, the focus/blur events will ## API -toggle() returns an array of instances. Each instance exposes the interface +Inititalisation returns an array of instances for each target element. Each instance exposes the interface ``` { - node, DOMElement, the text area + node, DOMElement, the element to expand and collapse startToggle, a Function that starts the toggle lifecycle with prehook, toggle, and post-toggle callback toggle, a Function that just executes the toggle getState, a Function that returns the current state Object @@ -127,7 +158,7 @@ There are two custom events that an instance of the toggle dispatches: - `toggle.open` when it opens - `toggle.close` when closes -The events are dispatched on the same element used to initialise the toggle and bubble for event delegation. The a reference to the getState function of the instance is contained in the custom event detail. +The events are dispatched on the same element used to initialise the toggle and bubble for event delegation. A reference to the getState function of the instance is contained in the custom event detail. ``` const [ instance ] = toggle('.js-toggle'); diff --git a/packages/toggle/__tests__/unit/broadcast.js b/packages/toggle/__tests__/unit/broadcast.js index 7d78ac58..3647958f 100644 --- a/packages/toggle/__tests__/unit/broadcast.js +++ b/packages/toggle/__tests__/unit/broadcast.js @@ -14,7 +14,7 @@ describe(`Toggle > broadcast`, () => { isOpen: true, settings: defaults }; - Store.dispatch(openState); + Store.update(openState); const listener = jest.fn(); const delegatedlistener = jest.fn(); node.addEventListener(EVENTS.OPEN, listener); diff --git a/packages/toggle/__tests__/unit/store.js b/packages/toggle/__tests__/unit/store.js index 3df6dc0a..b7d5d332 100644 --- a/packages/toggle/__tests__/unit/store.js +++ b/packages/toggle/__tests__/unit/store.js @@ -11,7 +11,7 @@ describe(`Toggle > Store`, () => { it('createStore should return an Object with an API', async () => { expect(Store).not.toBeNull(); expect(Store.getState).not.toBeNull(); - expect(Store.dispatch).not.toBeNull(); + expect(Store.update).not.toBeNull(); }); it('should have a getState function that returns a private state Object', async () => { @@ -19,20 +19,20 @@ describe(`Toggle > Store`, () => { expect(Store.getState()).toEqual({}); }); - it('should have a dispatch function that updates state', async () => { + it('should have an update function that updates state', async () => { const nextState = { isOpen: true }; - Store.dispatch(nextState); + Store.update(nextState); expect(Store.getState()).toEqual(nextState); }); - it('should have a dispatch function that does not update state if nextState is not passed', async () => { + it('should have an update function that does not update state if nextState is not passed', async () => { const Store = createStore(); - Store.dispatch(); + Store.update(); expect(Store.getState()).toEqual({}); }); - it('should have a dispatch function that invokes any side effect functions passed after the state change, with new state as only argument', async () => { - Store.dispatch({}, [sideEffect]); + it('should have an update function that invokes any side effect functions passed after the state change, with new state as only argument', async () => { + Store.update({}, [sideEffect]); expect(effect).toEqual(true); }); diff --git a/packages/toggle/example/src/index.html b/packages/toggle/example/src/index.html index 2587ff3a..b240a292 100644 --- a/packages/toggle/example/src/index.html +++ b/packages/toggle/example/src/index.html @@ -133,21 +133,5 @@ -
-

2. Local toggle

-
- -
-

Local toggles change the className of the target element's parentNode rather than the documentElement for isolating state to part of the DOM

- -
-
-
- -
-

Local toggles change the className of the target element's parentNode rather than the documentElement for isolating state to part of the DOM

- -
-
diff --git a/packages/toggle/example/src/js/index.js b/packages/toggle/example/src/js/index.js index 9a9975d0..52392f09 100644 --- a/packages/toggle/example/src/js/index.js +++ b/packages/toggle/example/src/js/index.js @@ -5,13 +5,6 @@ window.addEventListener('DOMContentLoaded', () => { focus: false, closeOnClick: true, closeOnBlur: true, - useHidden:true + useHidden: true }); - // window.__t2__ = toggle('.js-toggle__local', { - // closeOnBlur: true - // }); - - // document.addEventListener('Toggle.Open', e => { - // console.log(e.detail.getState()); - // }); }); \ No newline at end of file diff --git a/packages/toggle/src/index.js b/packages/toggle/src/index.js index 280dd929..09023f2c 100644 --- a/packages/toggle/src/index.js +++ b/packages/toggle/src/index.js @@ -9,11 +9,8 @@ import { getSelection } from './lib/utils'; * @params options, Object, to be merged with defaults to become the settings propery of each returned object */ export default (selector, options) => { - //Array.from isnt polyfilled - //https://github.com/babel/babel/issues/5682 let nodes = getSelection(selector); - //no DOM nodes found, return with warning if (nodes.length === 0) return console.warn(`Toggle not initialised, no elements found for selector '${selector}'`); //return array of Objects, one for each DOM node found diff --git a/packages/toggle/src/lib/constants.js b/packages/toggle/src/lib/constants.js index 5b301482..7ca12e55 100644 --- a/packages/toggle/src/lib/constants.js +++ b/packages/toggle/src/lib/constants.js @@ -1,7 +1,6 @@ /* istanbul ignore file */ export const ACCEPTED_TRIGGERS = ['button', 'a']; -//Array of focusable child elements export const FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex="-1"])']; export const EVENTS = { diff --git a/packages/toggle/src/lib/defaults.js b/packages/toggle/src/lib/defaults.js index a3e4bc22..19aa6bdc 100644 --- a/packages/toggle/src/lib/defaults.js +++ b/packages/toggle/src/lib/defaults.js @@ -2,9 +2,9 @@ /* * Default settings used by a Toggle instance if not otherwise overwritten with config * - * @property delay, Number, duration in milliseconds of toggle off process persistng animation state ('is--animating' on a global toggle) to support more granular off animations + * @property delay, Number, duration in milliseconds of toggle off process persisting animation state ('is--animating' on a global toggle) to support more granular off animations * @property startOpen, Boolean, toggle should start in an open state - * @property local, Boolean, toggle is localised in the DOM (claasName changes are made to the parentNode, not the documentElement) + * @property local, Boolean, toggle is localised in the DOM (className changes are made to the parentNode, not the documentElement) * @property prehook, Function, called before each toggle event begins * @property callback, Function, called after each toggle event completes * @property focus, Boolean, focus should change to the first focusable child of a toggled element when opened diff --git a/packages/toggle/src/lib/dom.js b/packages/toggle/src/lib/dom.js index 2bf5dcab..78b52fea 100644 --- a/packages/toggle/src/lib/dom.js +++ b/packages/toggle/src/lib/dom.js @@ -1,13 +1,15 @@ import { FOCUSABLE_ELEMENTS, ACCEPTED_TRIGGERS, EVENTS } from './constants'; /* - * Sets aria attributes and adds eventListener on each toggle button + * Partially applied function + * Sets aria attributes and adds eventListener to each toggle button * - * @param Store, Object, model or state of the current instance + * @param store, Object, model or state of the current instance + * @returns Function */ -export const initUI = Store => () => { - const { toggles, node, settings } = Store.getState(); - if(settings.useHidden) node.hidden = true; +export const initUI = store => () => { + const { toggles, node, settings } = store.getState(); + if (settings.useHidden) node.hidden = true; toggles.forEach(toggle => { const id = node.getAttribute('id'); if (toggle.tagName !== 'BUTTON') toggle.setAttribute('role', 'button'); @@ -17,36 +19,40 @@ export const initUI = Store => () => { toggle.addEventListener('click', e => { e.preventDefault(); - startToggleLifecycle(Store)(); + startToggleLifecycle(store)(); }); }); }; /* - * Dispatches a toggle action to the Store + * Partially applied function + * Dispatches a toggle action to the store * - * @param Store, Object, model or state of the current instance + * @param store, Object, model or state of the current instance + * @returns Function */ -export const toggle = Store => () => { - Store.dispatch({ - isOpen: !Store.getState().isOpen }, - [toggleAttributes, manageFocus(Store), closeProxy(Store), broadcast(Store)] +export const toggle = store => () => { + store.update({ + ...store.getState(), + isOpen: !store.getState().isOpen + }, + [ toggleAttributes, manageFocus(store), closeProxy(store), broadcast(store) ] ); }; /* * Partially applied function that returns a function that begins the toggle lifecycle (prehook > toggle > callback) * - * @param Store, Object, model or state of the current instance + * @param store, Object, model or state of the current instance * @returns Function */ -export const startToggleLifecycle = Store => () => { - const { node, toggles, settings, isOpen, classTarget, animatingClass } = Store.getState(); +export const startToggleLifecycle = store => () => { + const { node, toggles, settings, isOpen, classTarget, animatingClass } = store.getState(); (settings.prehook && typeof settings.prehook === 'function') && settings.prehook({ node, toggles, isOpen }); classTarget.classList.add(animatingClass); const fn = () => { - toggle(Store)(); - (!!settings.callback && typeof settings.callback === 'function') && settings.callback({ node, toggles, isOpen: Store.getState().isOpen }); + toggle(store)(); + (!!settings.callback && typeof settings.callback === 'function') && settings.callback({ node, toggles, isOpen: store.getState().isOpen }); }; if (isOpen && +settings.delay > 0) window.setTimeout(fn, +settings.delay); else fn(); @@ -61,7 +67,7 @@ export const startToggleLifecycle = Store => () => { export const findToggles = node => { const toggleSelector = node.getAttribute('data-toggle'); - const composeSelector = classSelector => { return ACCEPTED_TRIGGERS.map(sel => `${sel}.${classSelector}`).join(", "); } + const composeSelector = classSelector => ACCEPTED_TRIGGERS.map(sel => `${sel}.${classSelector}`).join(', '); const toggles = node.getAttribute('data-toggle') && [].slice.call(document.querySelectorAll(composeSelector(toggleSelector))); @@ -80,32 +86,31 @@ export const getFocusableChildren = node => [].slice.call(node.querySelectorAll( /* * Change toggle button attributes and node target classNames * - * @param props, Object, composed of properties of current state required to accessibly change node toggles attributes + * @param props, Object, composed of properties of current state required to accessibly change button and toggle attributes */ export const toggleAttributes = ({ toggles, isOpen, node, classTarget, animatingClass, statusClass, settings }) => { toggles.forEach(toggle => toggle.setAttribute('aria-expanded', isOpen)); classTarget.classList.remove(animatingClass); classTarget.classList[isOpen ? 'add' : 'remove'](statusClass); - if(settings.useHidden) node.hidden = !isOpen; + if (settings.useHidden) node.hidden = !isOpen; }; /* * Partially applied function that returns a handler function for keydown events when toggle is open - * Only added as an eventListener when trapTab option is set * - * @param Store, Object, model or store of the current instance + * @param store, Object, model or store of the current instance * @returns Function, keyboard event handler * * @param Event, document keydown event dispatched from document */ -export const keyListener = Store => e => { +export const keyListener = store => e => { switch (e.keyCode){ case 27: e.preventDefault(); - startToggleLifecycle(Store); + startToggleLifecycle(store); break; case 9: - trapTab(Store, e); + trapTab(store, e); break; } }; @@ -115,11 +120,11 @@ export const keyListener = Store => e => { * If shift is held focus set on the last focusable element * If last element, focus is set on the first element * - * @param Store, Object, model or store of the current instance + * @param store, Object, model or store of the current instance * @param e, Event, document keydown event passed down from keyListener */ -const trapTab = (Store, e) => { - const focusableChildren = Store.getState().focusableChildren; +const trapTab = (store, e) => { + const focusableChildren = store.getState().focusableChildren; const focusedIndex = focusableChildren.indexOf(document.activeElement); if (e.shiftKey && focusedIndex === 0) { e.preventDefault(); @@ -134,51 +139,53 @@ const trapTab = (Store, e) => { * Checks if the event was dispatched from a toggle button * * @param toggles, Array of toggle HTMLElements + * @param target, event target + * * @returns Boolean, true if event was dispatched from a toggle button * - */ + */ const targetIsToggle = (toggles, target) => toggles.reduce((acc, toggle) => { if (toggle === target|| toggle.contains(target)) acc = true; return acc; -}, false) +}, false); /* - * Partially applied factory function that returns handlers for focusin and click events - * Returned function is added as an eventListener when closeOnBlur options are true + * Partially applied factory function that returns handlers for focusin events + * Returned function is added as an eventListener when closeOnBlur option is true * - * @param Store, Object, model or store of the current instance + * @param store, Object, model or store of the current instance * @returns Function, event handler * * @param Event, event dispatched from document */ -export const focusInListener = Store => e => { - const state = Store.getState(); - if (!state.node.contains(e.target) && !targetIsToggle(state.toggles, e.target)) toggle(Store)(); +export const focusInListener = store => e => { + const state = store.getState(); + if (!state.node.contains(e.target) && !targetIsToggle(state.toggles, e.target)) toggle(store)(); }; /* * Partially applied factory function that returns handlers for focusin and click events * Returned function is added as an eventListener when closeOnClick options are true * - * @param Store, Object, model or store of the current instance + * @param store, Object, model or store of the current instance * @returns Function, event handler * * @param Event, event dispatched from document */ -export const clickListener = Store => e => { - const { node, toggles } = Store.getState(); +export const clickListener = store => e => { + const { node, toggles } = store.getState(); if (node.contains(e.target) || targetIsToggle(toggles, e.target)) return; - toggle(Store)(); + toggle(store)(); }; /* * Partially applied function that returns a function that adds and removes the document proxyListeners * Only added as an eventListener when closeOnBlur and/or closeOnClick options are true * - * @param Store, Object, model or state of the current instance + * @param store, Object, model or state of the current instance */ -export const closeProxy = Store => () => { - const { settings, isOpen, focusInListener, clickListener } = Store.getState(); +export const closeProxy = store => () => { + const { settings, isOpen, focusInListener, clickListener } = store.getState(); if (settings.closeOnBlur) document[`${isOpen ? 'add' : 'remove'}EventListener`]('focusin', focusInListener); if (settings.closeOnClick) document[`${isOpen ? 'add' : 'remove'}EventListener`]('click', clickListener); }; @@ -187,13 +194,13 @@ export const closeProxy = Store => () => { /* * Partially applied function that returns a function that sets up and pulls down focus event handlers based on toggle status and focus management options * - * @param Store, Object, model or state of the current instance + * @param store, Object, model or state of the current instance */ -export const manageFocus = Store => () => { - const { isOpen, focusableChildren, settings, lastFocused, keyListener } = Store.getState(); +export const manageFocus = store => () => { + const { isOpen, focusableChildren, settings, lastFocused, keyListener } = store.getState(); if ((!settings.focus && !settings.trapTab) || focusableChildren.length === 0) return; if (isOpen){ - Store.dispatch({ lastFocused: document.activeElement }); + store.update({ ...store.getState(), lastFocused: document.activeElement }); const focusFn = () => focusableChildren[0].focus(); if (settings.delay) window.setTimeout(focusFn, settings.delay); else focusFn(); @@ -204,7 +211,7 @@ export const manageFocus = Store => () => { document.removeEventListener('keydown', keyListener); const reFocusFn = () => { lastFocused && lastFocused.focus(); - Store.dispatch({ lastFocused: false }); + store.update({ ...store.getState(), lastFocused: false }); }; if (settings.delay) window.setTimeout(reFocusFn, settings.delay); else reFocusFn(); @@ -221,11 +228,11 @@ export const getStateFromDOM = (node, settings) => { }; }; -export const broadcast = Store => state => { +export const broadcast = store => state => { const event = new CustomEvent(EVENTS[state.isOpen ? 'OPEN' : 'CLOSE'], { bubbles: true, detail: { - getState: Store.getState + getState: store.getState } }); state.node.dispatchEvent(event); diff --git a/packages/toggle/src/lib/factory.js b/packages/toggle/src/lib/factory.js index 6bf526aa..68c96c7e 100644 --- a/packages/toggle/src/lib/factory.js +++ b/packages/toggle/src/lib/factory.js @@ -13,17 +13,17 @@ import { /* - * @param settings, Object, merged defaults + options passed in as instantiation config to module default + * @param settings, Object, merged defaults + options * @param node, HTMLElement, DOM node to be toggled * * @returns Object, Toggle API */ export default ({ node, settings }) => { - const Store = createStore(); + const store = createStore(); //resolve state from DOM const { classTarget, statusClass, shouldStartOpen } = getStateFromDOM(node, settings); - //set initial state of Store - Store.dispatch({ + //set initial state of store + store.update({ node, settings, toggles: findToggles(node), @@ -33,18 +33,18 @@ export default ({ node, settings }) => { animatingClass: settings.local ? `animating--${node.getAttribute('id')}` : 'is--animating', focusableChildren: getFocusableChildren(node), lastFocused: false, - keyListener: keyListener(Store), - focusInListener: focusInListener(Store), - clickListener: clickListener(Store) - }, [ initUI(Store), () => { - shouldStartOpen && startToggleLifecycle(Store)(); + keyListener: keyListener(store), + focusInListener: focusInListener(store), + clickListener: clickListener(store) + }, [ initUI(store), () => { + shouldStartOpen && startToggleLifecycle(store)(); }]); return { node, - startToggle: startToggleLifecycle(Store), - toggle: toggle(Store), - getState: Store.getState + startToggle: startToggleLifecycle(store), + toggle: toggle(store), + getState: store.getState }; }; \ No newline at end of file diff --git a/packages/toggle/src/lib/store.js b/packages/toggle/src/lib/store.js index 2fa1a04a..b0a62a25 100644 --- a/packages/toggle/src/lib/store.js +++ b/packages/toggle/src/lib/store.js @@ -1,29 +1,13 @@ export const createStore = () => { - //shared centralised validator state let state = {}; - - //uncomment for debugging by writing state history to window - // window.__validator_history__ = []; - - //state getter + const getState = () => state; - /** - * Create next state by invoking reducer on current state - * - * Execute side effects of state update, as passed in the update - * - * @param type [String] - * @param nextState [Object] New slice of state to combine with current state to create next state - * @param effects [Array] Array of side effect functions to invoke after state update (DOM, operations, cmds...) - */ - const dispatch = (nextState, effects) => { - state = nextState ? ({ ...state, ...nextState }) : state; - //uncomment for debugging by writing state history to window - // window.__validator_history__.push(state), console.log(window.__validator_history__); + const update = (nextState, effects) => { + state = nextState ?? state; if (!effects) return; - effects.forEach(effect => { effect(state); }); + effects.forEach(effect => effect(state)); }; - - return { dispatch, getState }; + + return { update, getState }; }; \ No newline at end of file diff --git a/packages/toggle/src/lib/utils.js b/packages/toggle/src/lib/utils.js index 55ac0900..270219c4 100644 --- a/packages/toggle/src/lib/utils.js +++ b/packages/toggle/src/lib/utils.js @@ -1,10 +1,9 @@ /* - * Converts a passed selector which can be of varying types into an array of DOM Objects + * Converts a selector of varying types into an array of DOM Objects * - * @param selector, Can be a string, Array of DOM nodes, a NodeList or a single DOM element. + * @param selector, string, Array of DOM nodes, a NodeList or a single DOM element. */ export const getSelection = selector => { - if (typeof selector === 'string') return [].slice.call(document.querySelectorAll(selector)); if (selector instanceof Array) return selector; if (Object.prototype.isPrototypeOf.call(NodeList.prototype, selector)) return [].slice.call(selector);