Skip to content

Commit

Permalink
Convert Protocol JS components to modern JS formats (Fixes #255)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexgibson committed Apr 18, 2023
1 parent e0566d7 commit 02c697e
Show file tree
Hide file tree
Showing 78 changed files with 14,009 additions and 10,373 deletions.
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ module.exports = {
extends: [
'eslint:recommended'
],
parserOptions: {
sourceType: 'module'
},
rules: {
// Require `let` or `const` instead of `var`
// https://eslint.org/docs/rules/no-var
Expand Down Expand Up @@ -71,5 +74,11 @@ module.exports = {
// Disallow the use of `console`
// https://eslint.org/docs/rules/no-console
'no-console': 'error'
},
globals: {
'MzpDetails': 'readable',
'MzpMenu': 'readable',
'MzpSupports': 'readable',
'MzpUtils': 'readable',
}
};
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# HEAD

## Features

* **js:** Protocol JS components are now written using modern JS and published as ES5/UMD format (#255).
* **js:** Removed pre-minified JS files from the published package. Consuming sites should handle their own minification.
* **css:** Removed pre-minified CSS files from the published package Consuming sites should handle their own minification.

## Bug Fixes

* **js:** Fix syntax error in legacy IE when compiling JS (#861)

# 16.1.0

## Features

* **component:** Add centering classes for Logo and Wordmark. (#718)
* **docs:** Migrate Protocol documentation site to Fractal.
* **node:** Create a Webpack config for compiling docs using Fractal.
Expand All @@ -15,6 +23,7 @@
* **docs:** Added 404 page

## Bug Fixes

* **js:** Ensure focus is moved to modal after animation completes (#829)
* **node:** Make sure to build NPM package using production mode.
* **html:** Added accessible attributes to menu bar (#815).
Expand All @@ -26,6 +35,7 @@
# 16.0.1

## Bug Fixes

* **css:** Fix repeating background on disabled search field (#767)
* **js:** Fix keyboard focus capture on modal open animation (#749).

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ $ npm install
Running `npm install` will install dependencies. Then:

```
$ npm run webpack-docs
$ npm run webpack
```

This will compile the Sass and copy assets into a local folder in preparation to
Expand Down
23 changes: 0 additions & 23 deletions assets/.eslintrc.js

This file was deleted.

22 changes: 22 additions & 0 deletions assets/js/protocol/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* This module is used to determine that JS is enabled in the browser,
* and provides `.js` and `.no-js` styling hooks used in component CSS.
* In order to avoid content flashing and repaints on page load, it is
* recommended that `MazBase.init();` should be run in the <head>, before
* page CSS is parsed.
*/

const MzpBase = {};

MzpBase.init = () => {
const doc = document.documentElement;

// Add class to reflect javascript availability for CSS
doc.className = doc.className.replace(/\bno-js\b/, 'js');
};

module.exports = MzpBase;
232 changes: 232 additions & 0 deletions assets/js/protocol/details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const MzpDetails = {};
let _count = 0;

MzpDetails.isSupported = () => {
if (typeof MzpSupports !== 'undefined' && typeof MzpUtils !== 'undefined') {
return MzpSupports.classList;
} else {
return false;
}
};

/**
* open
* @param {String} id - id of the container to open
* @param {Object} options - configurable options
*/
MzpDetails.open = (id, options) => {
const control = document.querySelector(`[aria-controls=${id}]`);
const details = document.getElementById(id);
control.setAttribute('aria-expanded', true);
details.setAttribute('aria-hidden', false);
details.classList.remove('is-closed');
if (typeof options.onDetailsOpen === 'function') {
options.onDetailsOpen(details);
}
};

/**
* close
* @param {String} id - id of the container to close
* @param {Object} options - configurable options
*/
MzpDetails.close = (id, options) => {
const control = document.querySelector(`[aria-controls=${id}]`);
const details = document.getElementById(id);
control.setAttribute('aria-expanded', false);
details.setAttribute('aria-hidden', true);
details.classList.add('is-closed');
if (typeof options.onDetailsClose === 'function') {
options.onDetailsClose(details);
}
};

/**
* toggle
* @param {String} id - id of the container to toggle
* @param {Object} options - configurable options
*/
MzpDetails.toggle = (id, options) => {
const details = document.getElementById(id);
const isClosed = details.getAttribute('aria-hidden');

if (isClosed === 'true') {
MzpDetails.open(id, options);
} else {
MzpDetails.close(id, options);
}
};

/**
* handleControlActivation
* @param {Event} e - event to handle
* @param {Object} options - configurable options
*/
MzpDetails.handleControlActivation = (e, options) => {
const control = e.target;
const id = control.getAttribute('aria-controls');
MzpDetails.toggle(id, options);
};

/**
* initItem
* @param {Object} el - Element to place the control inside of
* @param {String} selector - Selector for all control wrappers
* - assumes every sibling until the next control is associated with the control
* @param {Object} options - configurable options
*/
MzpDetails.initItem = (el, selector, options) => {
const summary = el;
const control = document.createElement('button');
let details;
const parent = summary.parentNode;

// if it's already been initialized, don't do it again
if (summary.querySelectorAll('button').length !== 0) {
return;
}

// Expand
// siblings of the summary, until next summary
const summarySiblings = MzpUtils.nextUntil(summary, selector);

// look to see if all children are already in a wrapper we can use
if (summarySiblings.length === 1) {
details = summarySiblings[0];
} else if (summarySiblings.length > 1){
details = document.createElement('div');
summarySiblings.forEach(function(sibling) {
details.appendChild(sibling);
});
summary.parentNode.insertBefore(details, summary.nextSibling);
} else {
// no children were found, something is probably wrong, let's stop here
return;
}

// add class to parent to indicate js initialized
parent.classList.add('is-details');

// add class to content wrapper
details.classList.add('mzp-js-details-wrapper');

// look for existing ID to use
if(!details.id) {
// if details already has ID, use that, if not assign one using the selector minus all not-letters
const unique = selector.replace(/[^a-zA-Z]+/g, '');
details.id = 'expand-' + unique + '-'+ _count;
_count += 1;
}

// close by default
// TODO: add support for open attribute
details.setAttribute('aria-hidden', true);
details.classList.add('is-closed');

// Control
control.setAttribute('type', 'button');
// add aria-controls
control.setAttribute('aria-controls', details.id);
// add aria-expanded
control.setAttribute('aria-expanded', false);
// add listener
control.addEventListener('click', function(e) {
MzpDetails.handleControlActivation(e, options);
}, false);
// copy the summary's contents into the control
const summaryChildren = Array.prototype.slice.call(summary.childNodes);
summaryChildren.forEach(function(child) {
control.appendChild(child);
});
// append control element
summary.appendChild(control);
summary.classList.add('is-summary');
};

/**
* destroyItem
* @param {Object} el - Element the control was placed inside of
* - does not attempt to remove the details wrapper
*/
MzpDetails.destroyItem = (el) => {
const summary = el;
const parent = summary.parentNode;
const details = summary.nextElementSibling;
const control = summary.querySelector('button');

// if it's already been destroyed, don't do it again
if (summary.querySelectorAll('button').length === 0) {
return;
}

parent.classList.remove('is-details');
details.removeAttribute('aria-hidden');
details.classList.remove('is-closed');
// move control's contents back to summary
const controlChildren = Array.prototype.slice.call(control.childNodes);
controlChildren.forEach(function(child) {
summary.appendChild(child);
});
summary.removeChild(control);
summary.classList.remove('is-summary');
};

/**
* Init
* @param {Object} selector - CSS selector matching "summary" elements
* @param {Object} options - configurable options
- passed in to the init function and passed around from there
example:
var testOptions = {
onDetailsOpen : myDetailsOpenCallback(),
onDetailsClose : function(){ //anonymous callback }
};
*/
MzpDetails.init = (selector, options) => {
if (!MzpDetails.isSupported()) {
return;
}
if (typeof options === 'undefined') {
options = {};
}

const summaries = document.querySelectorAll(selector);
// loop through controls on the page and init them one at a time
for (let i = 0; i < summaries.length; i++) {
MzpDetails.initItem(summaries[i], selector, options);
}
};

/**
* Destroy
* @param {Object} selector - CSS selector matching "summary" elements
* @param {Object} options - configurable options
*/
MzpDetails.destroy = (selector, options) => {
const summaries = document.querySelectorAll(selector, options);
// loop through controls on the page and destroy them one at a time
for (let i = 0; i < summaries.length; i++) {
MzpDetails.destroyItem(summaries[i], selector, options);
}
};

// check if details is supported, if not, init this as a polyfill
if (typeof MzpSupports !== 'undefined') {
// not supported, add support
if(!MzpSupports.details) {
MzpDetails.init('summary');
}
}

// init generic class indicating headings should be made into open/close component
MzpDetails.init('.mzp-c-details > h2');
MzpDetails.init('.mzp-c-details > h3');
MzpDetails.init('.mzp-c-details > h4');
MzpDetails.init('.mzp-c-details > h5');
MzpDetails.init('.mzp-c-details > h6');

module.exports = MzpDetails;
41 changes: 41 additions & 0 deletions assets/js/protocol/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const MzpFooter = {};

MzpFooter.init = () => {
const footerHeadings = '.mzp-c-footer-sections .mzp-c-footer-heading';

// removes Details component if screen size is big
function screenChange(mq) {
if (mq.matches) {
MzpDetails.init(footerHeadings);
} else {
MzpDetails.destroy(footerHeadings);
}
}

// check we have global Supports and Details library
if (typeof MzpSupports !== 'undefined' && typeof MzpDetails !== 'undefined') {

// check browser supports matchMedia
if (MzpSupports.matchMedia) {
const _mqWide = matchMedia('(max-width: 479px)');

// initialize details if screen is small
if (_mqWide.matches) {
MzpDetails.init(footerHeadings);
}

if (window.matchMedia('all').addEventListener) {
_mqWide.addEventListener('change', screenChange, false);
} else if (window.matchMedia('all').addListener) {
_mqWide.addListener(screenChange);
}
}
}

};

module.exports = MzpFooter;
Loading

0 comments on commit 02c697e

Please sign in to comment.