From 05b4004f5fa58253582cfe6b04e22a729512712e Mon Sep 17 00:00:00 2001 From: Reid Hewitt Date: Wed, 13 Dec 2023 11:26:24 -0700 Subject: [PATCH 1/4] update vendored copy. --- .../fanstatic_library/scripts/vendor/uswds.js | 3585 +++++++++-------- 1 file changed, 1918 insertions(+), 1667 deletions(-) diff --git a/ckanext/datagovtheme/fanstatic_library/scripts/vendor/uswds.js b/ckanext/datagovtheme/fanstatic_library/scripts/vendor/uswds.js index 10ea13e0..a1b638d3 100644 --- a/ckanext/datagovtheme/fanstatic_library/scripts/vendor/uswds.js +++ b/ckanext/datagovtheme/fanstatic_library/scripts/vendor/uswds.js @@ -3,20 +3,17 @@ /* * classList.js: Cross-browser full element.classList implementation. - * 2014-07-23 + * 1.1.20170427 * * By Eli Grey, http://eligrey.com - * Public Domain. - * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + * License: Dedicated to the public domain. + * See https://github.com/eligrey/classList.js/blob/master/LICENSE.md */ /*global self, document, DOMException */ -/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ +/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ -/* Copied from MDN: - * https://developer.mozilla.org/en-US/docs/Web/API/Element/classList - */ if ("document" in window.self) { // Full polyfill for browsers with no classList support // Including IE < Edge missing SVGElement.classList @@ -25,152 +22,131 @@ if ("document" in window.self) { "use strict"; if (!('Element' in view)) return; - var classListProp = "classList", - protoProp = "prototype", - elemCtrProto = view.Element[protoProp], - objCtr = Object, - strTrim = String[protoProp].trim || function () { - return this.replace(/^\s+|\s+$/g, ""); - }, - arrIndexOf = Array[protoProp].indexOf || function (item) { - var i = 0, + protoProp = "prototype", + elemCtrProto = view.Element[protoProp], + objCtr = Object, + strTrim = String[protoProp].trim || function () { + return this.replace(/^\s+|\s+$/g, ""); + }, + arrIndexOf = Array[protoProp].indexOf || function (item) { + var i = 0, len = this.length; - - for (; i < len; i++) { - if (i in this && this[i] === item) { - return i; + for (; i < len; i++) { + if (i in this && this[i] === item) { + return i; + } } + return -1; } - - return -1; - } // Vendors: please allow content code to instantiate DOMExceptions - , - DOMEx = function (type, message) { - this.name = type; - this.code = DOMException[type]; - this.message = message; - }, - checkTokenAndGetIndex = function (classList, token) { - if (token === "") { - throw new DOMEx("SYNTAX_ERR", "An invalid or illegal string was specified"); - } - - if (/\s/.test(token)) { - throw new DOMEx("INVALID_CHARACTER_ERR", "String contains an invalid character"); - } - - return arrIndexOf.call(classList, token); - }, - ClassList = function (elem) { - var trimmedClasses = strTrim.call(elem.getAttribute("class") || ""), + // Vendors: please allow content code to instantiate DOMExceptions + , + DOMEx = function (type, message) { + this.name = type; + this.code = DOMException[type]; + this.message = message; + }, + checkTokenAndGetIndex = function (classList, token) { + if (token === "") { + throw new DOMEx("SYNTAX_ERR", "An invalid or illegal string was specified"); + } + if (/\s/.test(token)) { + throw new DOMEx("INVALID_CHARACTER_ERR", "String contains an invalid character"); + } + return arrIndexOf.call(classList, token); + }, + ClassList = function (elem) { + var trimmedClasses = strTrim.call(elem.getAttribute("class") || ""), classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [], i = 0, len = classes.length; - - for (; i < len; i++) { - this.push(classes[i]); - } - - this._updateClassName = function () { - elem.setAttribute("class", this.toString()); + for (; i < len; i++) { + this.push(classes[i]); + } + this._updateClassName = function () { + elem.setAttribute("class", this.toString()); + }; + }, + classListProto = ClassList[protoProp] = [], + classListGetter = function () { + return new ClassList(this); }; - }, - classListProto = ClassList[protoProp] = [], - classListGetter = function () { - return new ClassList(this); - }; // Most DOMException implementations don't allow calling DOMException's toString() + // Most DOMException implementations don't allow calling DOMException's toString() // on non-DOMExceptions. Error's toString() is sufficient here. - - DOMEx[protoProp] = Error[protoProp]; - classListProto.item = function (i) { return this[i] || null; }; - classListProto.contains = function (token) { token += ""; return checkTokenAndGetIndex(this, token) !== -1; }; - classListProto.add = function () { var tokens = arguments, - i = 0, - l = tokens.length, - token, - updated = false; - + i = 0, + l = tokens.length, + token, + updated = false; do { token = tokens[i] + ""; - if (checkTokenAndGetIndex(this, token) === -1) { this.push(token); updated = true; } } while (++i < l); - if (updated) { this._updateClassName(); } }; - classListProto.remove = function () { var tokens = arguments, - i = 0, - l = tokens.length, - token, - updated = false, - index; - + i = 0, + l = tokens.length, + token, + updated = false, + index; do { token = tokens[i] + ""; index = checkTokenAndGetIndex(this, token); - while (index !== -1) { this.splice(index, 1); updated = true; index = checkTokenAndGetIndex(this, token); } } while (++i < l); - if (updated) { this._updateClassName(); } }; - classListProto.toggle = function (token, force) { token += ""; var result = this.contains(token), - method = result ? force !== true && "remove" : force !== false && "add"; - + method = result ? force !== true && "remove" : force !== false && "add"; if (method) { this[method](token); } - if (force === true || force === false) { return force; } else { return !result; } }; - classListProto.toString = function () { return this.join(" "); }; - if (objCtr.defineProperty) { var classListPropDesc = { get: classListGetter, enumerable: true, configurable: true }; - try { objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } catch (ex) { // IE 8 doesn't support enumerable:true - if (ex.number === -0x7FF5EC54) { + // adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36 + // modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected + if (ex.number === undefined || ex.number === -0x7FF5EC54) { classListPropDesc.enumerable = false; objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); } @@ -179,121 +155,88 @@ if ("document" in window.self) { elemCtrProto.__defineGetter__(classListProp, classListGetter); } })(window.self); - } else { - // There is full or partial native classList support, so just check if we need - // to normalize the add/remove and toggle APIs. - (function () { - "use strict"; - - var testElement = document.createElement("_"); - testElement.classList.add("c1", "c2"); // Polyfill for IE 10/11 and Firefox <26, where classList.add and - // classList.remove exist but support only one argument at a time. - - if (!testElement.classList.contains("c2")) { - var createMethod = function (method) { - var original = DOMTokenList.prototype[method]; - - DOMTokenList.prototype[method] = function (token) { - var i, - len = arguments.length; - - for (i = 0; i < len; i++) { - token = arguments[i]; - original.call(this, token); - } - }; - }; + } - createMethod('add'); - createMethod('remove'); - } + // There is full or partial native classList support, so just check if we need + // to normalize the add/remove and toggle APIs. - testElement.classList.toggle("c3", false); // Polyfill for IE 10 and Firefox <24, where classList.toggle does not - // support the second argument. + (function () { + "use strict"; - if (testElement.classList.contains("c3")) { - var _toggle = DOMTokenList.prototype.toggle; + var testElement = document.createElement("_"); + testElement.classList.add("c1", "c2"); - DOMTokenList.prototype.toggle = function (token, force) { - if (1 in arguments && !this.contains(token) === !force) { - return force; - } else { - return _toggle.call(this, token); + // Polyfill for IE 10/11 and Firefox <26, where classList.add and + // classList.remove exist but support only one argument at a time. + if (!testElement.classList.contains("c2")) { + var createMethod = function (method) { + var original = DOMTokenList.prototype[method]; + DOMTokenList.prototype[method] = function (token) { + var i, + len = arguments.length; + for (i = 0; i < len; i++) { + token = arguments[i]; + original.call(this, token); } }; - } - - testElement = null; - })(); - } + }; + createMethod('add'); + createMethod('remove'); + } + testElement.classList.toggle("c3", false); + + // Polyfill for IE 10 and Firefox <24, where classList.toggle does not + // support the second argument. + if (testElement.classList.contains("c3")) { + var _toggle = DOMTokenList.prototype.toggle; + DOMTokenList.prototype.toggle = function (token, force) { + if (1 in arguments && !this.contains(token) === !force) { + return force; + } else { + return _toggle.call(this, token); + } + }; + } + testElement = null; + })(); } },{}],2:[function(require,module,exports){ "use strict"; -/*! - * domready (c) Dustin Diaz 2014 - License MIT - */ -!function (name, definition) { - if (typeof module != 'undefined') module.exports = definition();else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);else this[name] = definition(); -}('domready', function () { - var fns = [], - listener, - doc = document, - hack = doc.documentElement.doScroll, - domContentLoaded = 'DOMContentLoaded', - loaded = (hack ? /^loaded|^c/ : /^loaded|^i|^c/).test(doc.readyState); - if (!loaded) doc.addEventListener(domContentLoaded, listener = function () { - doc.removeEventListener(domContentLoaded, listener); - loaded = 1; - - while (listener = fns.shift()) listener(); - }); - return function (fn) { - loaded ? setTimeout(fn, 0) : fns.push(fn); - }; -}); - -},{}],3:[function(require,module,exports){ -"use strict"; - // element-closest | CC0-1.0 | github.com/jonathantneal/closest + (function (ElementProto) { if (typeof ElementProto.matches !== 'function') { ElementProto.matches = ElementProto.msMatchesSelector || ElementProto.mozMatchesSelector || ElementProto.webkitMatchesSelector || function matches(selector) { var element = this; var elements = (element.document || element.ownerDocument).querySelectorAll(selector); var index = 0; - while (elements[index] && elements[index] !== element) { ++index; } - return Boolean(elements[index]); }; } - if (typeof ElementProto.closest !== 'function') { ElementProto.closest = function closest(selector) { var element = this; - while (element && element.nodeType === 1) { if (element.matches(selector)) { return element; } - element = element.parentNode; } - return null; }; } })(window.Element.prototype); -},{}],4:[function(require,module,exports){ +},{}],3:[function(require,module,exports){ "use strict"; /* global define, KeyboardEvent, module */ + (function () { var keyboardeventKeyPolyfill = { polyfill: polyfill, @@ -366,43 +309,38 @@ if ("document" in window.self) { 250: 'Play', 251: 'ZoomOut' } - }; // Function keys (F1-24). + }; + // Function keys (F1-24). var i; - for (i = 1; i < 25; i++) { keyboardeventKeyPolyfill.keys[111 + i] = 'F' + i; - } // Printable ASCII characters. - + } + // Printable ASCII characters. var letter = ''; - for (i = 65; i < 91; i++) { letter = String.fromCharCode(i); keyboardeventKeyPolyfill.keys[i] = [letter.toLowerCase(), letter.toUpperCase()]; } - function polyfill() { if (!('KeyboardEvent' in window) || 'key' in KeyboardEvent.prototype) { return false; - } // Polyfill `key` on `KeyboardEvent`. - + } + // Polyfill `key` on `KeyboardEvent`. var proto = { get: function (x) { var key = keyboardeventKeyPolyfill.keys[this.which || this.keyCode]; - if (Array.isArray(key)) { key = key[+this.shiftKey]; } - return key; } }; Object.defineProperty(KeyboardEvent.prototype, 'key', proto); return proto; } - if (typeof define === 'function' && define.amd) { define('keyboardevent-key-polyfill', keyboardeventKeyPolyfill); } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { @@ -412,92 +350,79 @@ if ("document" in window.self) { } })(); -},{}],5:[function(require,module,exports){ +},{}],4:[function(require,module,exports){ /* object-assign (c) Sindre Sorhus @license MIT */ + 'use strict'; -/* eslint-disable no-unused-vars */ +/* eslint-disable no-unused-vars */ var getOwnPropertySymbols = Object.getOwnPropertySymbols; var hasOwnProperty = Object.prototype.hasOwnProperty; var propIsEnumerable = Object.prototype.propertyIsEnumerable; - function toObject(val) { if (val === null || val === undefined) { throw new TypeError('Object.assign cannot be called with null or undefined'); } - return Object(val); } - function shouldUseNative() { try { if (!Object.assign) { return false; - } // Detect buggy property enumeration order in older V8 versions. - // https://bugs.chromium.org/p/v8/issues/detail?id=4118 + } + // Detect buggy property enumeration order in older V8 versions. + // https://bugs.chromium.org/p/v8/issues/detail?id=4118 var test1 = new String('abc'); // eslint-disable-line no-new-wrappers - test1[5] = 'de'; - if (Object.getOwnPropertyNames(test1)[0] === '5') { return false; - } // https://bugs.chromium.org/p/v8/issues/detail?id=3056 - + } + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 var test2 = {}; - for (var i = 0; i < 10; i++) { test2['_' + String.fromCharCode(i)] = i; } - var order2 = Object.getOwnPropertyNames(test2).map(function (n) { return test2[n]; }); - if (order2.join('') !== '0123456789') { return false; - } // https://bugs.chromium.org/p/v8/issues/detail?id=3056 - + } + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 var test3 = {}; 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { test3[letter] = letter; }); - if (Object.keys(Object.assign({}, test3)).join('') !== 'abcdefghijklmnopqrst') { return false; } - return true; } catch (err) { // We don't expect any of the above to throw, but better to be safe. return false; } } - module.exports = shouldUseNative() ? Object.assign : function (target, source) { var from; var to = toObject(target); var symbols; - for (var s = 1; s < arguments.length; s++) { from = Object(arguments[s]); - for (var key in from) { if (hasOwnProperty.call(from, key)) { to[key] = from[key]; } } - if (getOwnPropertySymbols) { symbols = getOwnPropertySymbols(from); - for (var i = 0; i < symbols.length; i++) { if (propIsEnumerable.call(from, symbols[i])) { to[symbols[i]] = from[symbols[i]]; @@ -505,46 +430,36 @@ module.exports = shouldUseNative() ? Object.assign : function (target, source) { } } } - return to; }; -},{}],6:[function(require,module,exports){ +},{}],5:[function(require,module,exports){ "use strict"; const assign = require('object-assign'); - const delegate = require('../delegate'); - const delegateAll = require('../delegateAll'); - const DELEGATE_PATTERN = /^(.+):delegate\((.+)\)$/; const SPACE = ' '; - const getListeners = function (type, handler) { var match = type.match(DELEGATE_PATTERN); var selector; - if (match) { type = match[1]; selector = match[2]; } - var options; - if (typeof handler === 'object') { options = { capture: popKey(handler, 'capture'), passive: popKey(handler, 'passive') }; } - var listener = { selector: selector, delegate: typeof handler === 'object' ? delegateAll(handler) : selector ? delegate(selector, handler) : handler, options: options }; - if (type.indexOf(SPACE) > -1) { return type.split(SPACE).map(function (_type) { return assign({ @@ -556,13 +471,11 @@ const getListeners = function (type, handler) { return [listener]; } }; - var popKey = function (obj, key) { var value = obj[key]; delete obj[key]; return value; }; - module.exports = function behavior(events, props) { const listeners = Object.keys(events).reduce(function (memo, type) { var listeners = getListeners(type, events[type]); @@ -582,7 +495,7 @@ module.exports = function behavior(events, props) { }, props); }; -},{"../delegate":8,"../delegateAll":9,"object-assign":5}],7:[function(require,module,exports){ +},{"../delegate":7,"../delegateAll":8,"object-assign":4}],6:[function(require,module,exports){ "use strict"; module.exports = function compose(functions) { @@ -593,40 +506,35 @@ module.exports = function compose(functions) { }; }; -},{}],8:[function(require,module,exports){ +},{}],7:[function(require,module,exports){ "use strict"; // polyfill Element.prototype.closest require('element-closest'); - module.exports = function delegate(selector, fn) { return function delegation(event) { var target = event.target.closest(selector); - if (target) { return fn.call(target, event); } }; }; -},{"element-closest":3}],9:[function(require,module,exports){ +},{"element-closest":2}],8:[function(require,module,exports){ "use strict"; const delegate = require('../delegate'); - const compose = require('../compose'); - const SPLAT = '*'; - module.exports = function delegateAll(selectors) { - const keys = Object.keys(selectors); // XXX optimization: if there is only one handler and it applies to + const keys = Object.keys(selectors); + + // XXX optimization: if there is only one handler and it applies to // all elements (the "*" CSS selector), then just return that // handler - if (keys.length === 1 && keys[0] === SPLAT) { return selectors[SPLAT]; } - const delegates = keys.reduce(function (memo, selector) { memo.push(delegate(selector, selectors[selector])); return memo; @@ -634,7 +542,7 @@ module.exports = function delegateAll(selectors) { return compose(delegates); }; -},{"../compose":7,"../delegate":8}],10:[function(require,module,exports){ +},{"../compose":6,"../delegate":7}],9:[function(require,module,exports){ "use strict"; module.exports = function ignore(element, fn) { @@ -645,7 +553,7 @@ module.exports = function ignore(element, fn) { }; }; -},{}],11:[function(require,module,exports){ +},{}],10:[function(require,module,exports){ "use strict"; module.exports = { @@ -656,14 +564,14 @@ module.exports = { keymap: require('./keymap') }; -},{"./behavior":6,"./delegate":8,"./delegateAll":9,"./ignore":10,"./keymap":12}],12:[function(require,module,exports){ +},{"./behavior":5,"./delegate":7,"./delegateAll":8,"./ignore":9,"./keymap":11}],11:[function(require,module,exports){ "use strict"; -require('keyboardevent-key-polyfill'); // these are the only relevant modifiers supported on all platforms, +require('keyboardevent-key-polyfill'); + +// these are the only relevant modifiers supported on all platforms, // according to MDN: // - - const MODIFIERS = { 'Alt': 'altKey', 'Control': 'ctrlKey', @@ -671,10 +579,8 @@ const MODIFIERS = { 'Shift': 'shiftKey' }; const MODIFIER_SEPARATOR = '+'; - const getEventKey = function (event, hasModifiers) { var key = event.key; - if (hasModifiers) { for (var modifier in MODIFIERS) { if (event[MODIFIERS[modifier]] === true) { @@ -682,10 +588,8 @@ const getEventKey = function (event, hasModifiers) { } } } - return key; }; - module.exports = function keymap(keys) { const hasModifiers = Object.keys(keys).some(function (key) { return key.indexOf(MODIFIER_SEPARATOR) > -1; @@ -696,15 +600,13 @@ module.exports = function keymap(keys) { if (_key in keys) { result = keys[key].call(this, event); } - return result; }, undefined); }; }; - module.exports.MODIFIERS = MODIFIERS; -},{"keyboardevent-key-polyfill":4}],13:[function(require,module,exports){ +},{"keyboardevent-key-polyfill":3}],12:[function(require,module,exports){ "use strict"; module.exports = function once(listener, options) { @@ -712,11 +614,10 @@ module.exports = function once(listener, options) { e.currentTarget.removeEventListener(e.type, wrapped, options); return listener.call(this, e); }; - return wrapped; }; -},{}],14:[function(require,module,exports){ +},{}],13:[function(require,module,exports){ 'use strict'; var RE_TRIM = /(^\s+)|(\s+$)/g; @@ -726,102 +627,86 @@ var trim = String.prototype.trim ? function (str) { } : function (str) { return str.replace(RE_TRIM, ''); }; - var queryById = function (id) { return this.querySelector('[id="' + id.replace(/"/g, '\\"') + '"]'); }; - module.exports = function resolveIds(ids, doc) { if (typeof ids !== 'string') { throw new Error('Expected a string but got ' + typeof ids); } - if (!doc) { doc = window.document; } - var getElementById = doc.getElementById ? doc.getElementById.bind(doc) : queryById.bind(doc); - ids = trim(ids).split(RE_SPLIT); // XXX we can short-circuit here because trimming and splitting a + ids = trim(ids).split(RE_SPLIT); + + // XXX we can short-circuit here because trimming and splitting a // string of just whitespace produces an array containing a single, // empty string - if (ids.length === 1 && ids[0] === '') { return []; } - return ids.map(function (id) { var el = getElementById(id); - if (!el) { throw new Error('no element with id: "' + id + '"'); } - return el; }); }; -},{}],15:[function(require,module,exports){ +},{}],14:[function(require,module,exports){ "use strict"; const behavior = require("../../uswds-core/src/js/utils/behavior"); - const toggleFormInput = require("../../uswds-core/src/js/utils/toggle-form-input"); - const { CLICK } = require("../../uswds-core/src/js/events"); - const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - const LINK = `.${PREFIX}-show-password`; - function toggle(event) { event.preventDefault(); toggleFormInput(this); } - module.exports = behavior({ [CLICK]: { [LINK]: toggle } }); -},{"../../uswds-core/src/js/config":33,"../../uswds-core/src/js/events":34,"../../uswds-core/src/js/utils/behavior":43,"../../uswds-core/src/js/utils/toggle-form-input":52}],16:[function(require,module,exports){ +},{"../../uswds-core/src/js/config":36,"../../uswds-core/src/js/events":37,"../../uswds-core/src/js/utils/behavior":46,"../../uswds-core/src/js/utils/toggle-form-input":56}],15:[function(require,module,exports){ "use strict"; const select = require("../../uswds-core/src/js/utils/select"); - const behavior = require("../../uswds-core/src/js/utils/behavior"); - const toggle = require("../../uswds-core/src/js/utils/toggle"); - const isElementInViewport = require("../../uswds-core/src/js/utils/is-in-viewport"); - const { CLICK } = require("../../uswds-core/src/js/events"); - const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - const ACCORDION = `.${PREFIX}-accordion, .${PREFIX}-accordion--bordered`; -const BUTTON = `.${PREFIX}-accordion__button[aria-controls]`; +const BANNER_BUTTON = `.${PREFIX}-banner__button`; +const BUTTON = `.${PREFIX}-accordion__button[aria-controls]:not(${BANNER_BUTTON})`; const EXPANDED = "aria-expanded"; const MULTISELECTABLE = "data-allow-multiple"; + /** * Get an Array of button elements belonging directly to the given * accordion element. * @param {HTMLElement} accordion * @return {array} */ - const getAccordionButtons = accordion => { const buttons = select(BUTTON, accordion); return buttons.filter(button => button.closest(ACCORDION) === accordion); }; + /** * Toggle a button's "pressed" state, optionally providing a target * state. @@ -831,20 +716,16 @@ const getAccordionButtons = accordion => { * state will be toggled (from false to true, and vice-versa). * @return {boolean} the resulting state */ - - const toggleButton = (button, expanded) => { const accordion = button.closest(ACCORDION); let safeExpanded = expanded; - if (!accordion) { throw new Error(`${BUTTON} is missing outer ${ACCORDION}`); } + safeExpanded = toggle(button, expanded); - safeExpanded = toggle(button, expanded); // XXX multiselectable is opt-in, to preserve legacy behavior - + // XXX multiselectable is opt-in, to preserve legacy behavior const multiselectable = accordion.hasAttribute(MULTISELECTABLE); - if (safeExpanded && !multiselectable) { getAccordionButtons(accordion).forEach(other => { if (other !== button) { @@ -853,26 +734,22 @@ const toggleButton = (button, expanded) => { }); } }; + /** * @param {HTMLButtonElement} button * @return {boolean} true */ - - const showButton = button => toggleButton(button, true); + /** * @param {HTMLButtonElement} button * @return {boolean} false */ - - const hideButton = button => toggleButton(button, false); - const accordion = behavior({ [CLICK]: { - [BUTTON](event) { + [BUTTON]() { toggleButton(this); - if (this.getAttribute(EXPANDED) === "true") { // We were just expanded, but if another accordion was also just // collapsed, we may no longer be in the viewport. This ensures @@ -880,7 +757,6 @@ const accordion = behavior({ if (!isElementInViewport(this)) this.scrollIntoView(); } } - } }, { init(root) { @@ -889,7 +765,6 @@ const accordion = behavior({ toggleButton(button, expanded); }); }, - ACCORDION, BUTTON, show: showButton, @@ -899,176 +774,255 @@ const accordion = behavior({ }); module.exports = accordion; -},{"../../uswds-core/src/js/config":33,"../../uswds-core/src/js/events":34,"../../uswds-core/src/js/utils/behavior":43,"../../uswds-core/src/js/utils/is-in-viewport":45,"../../uswds-core/src/js/utils/select":50,"../../uswds-core/src/js/utils/toggle":53}],17:[function(require,module,exports){ +},{"../../uswds-core/src/js/config":36,"../../uswds-core/src/js/events":37,"../../uswds-core/src/js/utils/behavior":46,"../../uswds-core/src/js/utils/is-in-viewport":49,"../../uswds-core/src/js/utils/select":54,"../../uswds-core/src/js/utils/toggle":57}],16:[function(require,module,exports){ "use strict"; const behavior = require("../../uswds-core/src/js/utils/behavior"); - +const select = require("../../uswds-core/src/js/utils/select"); const { CLICK } = require("../../uswds-core/src/js/events"); - const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - +const toggle = require("../../uswds-core/src/js/utils/toggle"); const HEADER = `.${PREFIX}-banner__header`; const EXPANDED_CLASS = `${PREFIX}-banner__header--expanded`; +const BANNER_BUTTON = `${HEADER} [aria-controls]`; +/** + * Toggle Banner display and class. + * @param {Event} event + */ const toggleBanner = function toggleEl(event) { event.preventDefault(); + const trigger = event.target.closest(BANNER_BUTTON); + toggle(trigger); this.closest(HEADER).classList.toggle(EXPANDED_CLASS); }; - module.exports = behavior({ [CLICK]: { - [`${HEADER} [aria-controls]`]: toggleBanner + [BANNER_BUTTON]: toggleBanner + } +}, { + init(root) { + select(BANNER_BUTTON, root).forEach(button => { + const expanded = button.getAttribute(EXPANDED_CLASS) === "true"; + toggle(button, expanded); + }); } }); -},{"../../uswds-core/src/js/config":33,"../../uswds-core/src/js/events":34,"../../uswds-core/src/js/utils/behavior":43}],18:[function(require,module,exports){ +},{"../../uswds-core/src/js/config":36,"../../uswds-core/src/js/events":37,"../../uswds-core/src/js/utils/behavior":46,"../../uswds-core/src/js/utils/select":54,"../../uswds-core/src/js/utils/toggle":57}],17:[function(require,module,exports){ "use strict"; -const select = require("../../uswds-core/src/js/utils/select"); - +const keymap = require("receptor/keymap"); const behavior = require("../../uswds-core/src/js/utils/behavior"); +const ANCHOR_BUTTON = `a[class*="usa-button"]`; +const toggleButton = event => { + event.preventDefault(); + event.target.click(); +}; +const anchorButton = behavior({ + keydown: { + [ANCHOR_BUTTON]: keymap({ + " ": toggleButton + }) + } +}); +module.exports = anchorButton; + +},{"../../uswds-core/src/js/utils/behavior":46,"receptor/keymap":11}],18:[function(require,module,exports){ +"use strict"; +const select = require("../../uswds-core/src/js/utils/select"); +const behavior = require("../../uswds-core/src/js/utils/behavior"); +const debounce = require("../../uswds-core/src/js/utils/debounce"); const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - -const CHARACTER_COUNT = `.${PREFIX}-character-count`; +const CHARACTER_COUNT_CLASS = `${PREFIX}-character-count`; +const CHARACTER_COUNT = `.${CHARACTER_COUNT_CLASS}`; const INPUT = `.${PREFIX}-character-count__field`; const MESSAGE = `.${PREFIX}-character-count__message`; const VALIDATION_MESSAGE = "The content is too long."; -const MESSAGE_INVALID_CLASS = `${PREFIX}-character-count__message--invalid`; -/** - * The elements within the character count. - * @typedef {Object} CharacterCountElements - * @property {HTMLDivElement} characterCountEl - * @property {HTMLSpanElement} messageEl - */ +const MESSAGE_INVALID_CLASS = `${PREFIX}-character-count__status--invalid`; +const STATUS_MESSAGE_CLASS = `${CHARACTER_COUNT_CLASS}__status`; +const STATUS_MESSAGE_SR_ONLY_CLASS = `${CHARACTER_COUNT_CLASS}__sr-status`; +const STATUS_MESSAGE = `.${STATUS_MESSAGE_CLASS}`; +const STATUS_MESSAGE_SR_ONLY = `.${STATUS_MESSAGE_SR_ONLY_CLASS}`; +const DEFAULT_STATUS_LABEL = `characters allowed`; /** - * Returns the root and message element - * for an character count input + * Returns the root and message element for an character count input * * @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element * @returns {CharacterCountElements} elements The root and message element. */ - const getCharacterCountElements = inputEl => { const characterCountEl = inputEl.closest(CHARACTER_COUNT); - if (!characterCountEl) { throw new Error(`${INPUT} is missing outer ${CHARACTER_COUNT}`); } - const messageEl = characterCountEl.querySelector(MESSAGE); - if (!messageEl) { throw new Error(`${CHARACTER_COUNT} is missing inner ${MESSAGE}`); } - return { characterCountEl, messageEl }; }; + /** - * Update the character count component + * Move maxlength attribute to a data attribute on usa-character-count * * @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element */ - - -const updateCountMessage = inputEl => { +const setDataLength = inputEl => { const { - characterCountEl, - messageEl + characterCountEl } = getCharacterCountElements(inputEl); - const maxlength = parseInt(characterCountEl.getAttribute("data-maxlength"), 10); + const maxlength = inputEl.getAttribute("maxlength"); if (!maxlength) return; - let newMessage = ""; - const currentLength = inputEl.value.length; - const isOverLimit = currentLength && currentLength > maxlength; + inputEl.removeAttribute("maxlength"); + characterCountEl.setAttribute("data-maxlength", maxlength); +}; + +/** + * Create and append status messages for visual and screen readers + * + * @param {HTMLDivElement} characterCountEl - Div with `.usa-character-count` class + * @description Create two status messages for number of characters left; + * one visual status and another for screen readers + */ +const createStatusMessages = characterCountEl => { + const statusMessage = document.createElement("div"); + const srStatusMessage = document.createElement("div"); + const maxLength = characterCountEl.dataset.maxlength; + const defaultMessage = `${maxLength} ${DEFAULT_STATUS_LABEL}`; + statusMessage.classList.add(`${STATUS_MESSAGE_CLASS}`, "usa-hint"); + srStatusMessage.classList.add(`${STATUS_MESSAGE_SR_ONLY_CLASS}`, "usa-sr-only"); + statusMessage.setAttribute("aria-hidden", true); + srStatusMessage.setAttribute("aria-live", "polite"); + statusMessage.textContent = defaultMessage; + srStatusMessage.textContent = defaultMessage; + characterCountEl.append(statusMessage, srStatusMessage); +}; +/** + * Returns message with how many characters are left + * + * @param {number} currentLength - The number of characters used + * @param {number} maxLength - The total number of characters allowed + * @returns {string} A string description of how many characters are left + */ +const getCountMessage = (currentLength, maxLength) => { + let newMessage = ""; if (currentLength === 0) { - newMessage = `${maxlength} characters allowed`; + newMessage = `${maxLength} ${DEFAULT_STATUS_LABEL}`; } else { - const difference = Math.abs(maxlength - currentLength); + const difference = Math.abs(maxLength - currentLength); const characters = `character${difference === 1 ? "" : "s"}`; - const guidance = isOverLimit ? "over limit" : "left"; + const guidance = currentLength > maxLength ? "over limit" : "left"; newMessage = `${difference} ${characters} ${guidance}`; } - - messageEl.classList.toggle(MESSAGE_INVALID_CLASS, isOverLimit); - messageEl.textContent = newMessage; - - if (isOverLimit && !inputEl.validationMessage) { - inputEl.setCustomValidity(VALIDATION_MESSAGE); - } - - if (!isOverLimit && inputEl.validationMessage === VALIDATION_MESSAGE) { - inputEl.setCustomValidity(""); - } + return newMessage; }; + /** - * Setup the character count component + * Updates the character count status for screen readers after a 1000ms delay. * - * @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element + * @param {HTMLElement} msgEl - The screen reader status message element + * @param {string} statusMessage - A string of the current character status */ +const srUpdateStatus = debounce((msgEl, statusMessage) => { + const srStatusMessage = msgEl; + srStatusMessage.textContent = statusMessage; +}, 1000); - -const setupAttributes = inputEl => { +/** + * Update the character count component + * + * @description On input, it will update visual status, screenreader + * status and update input validation (if over character length) + * @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element + */ +const updateCountMessage = inputEl => { const { characterCountEl } = getCharacterCountElements(inputEl); - const maxlength = inputEl.getAttribute("maxlength"); - if (!maxlength) return; - inputEl.removeAttribute("maxlength"); - characterCountEl.setAttribute("data-maxlength", maxlength); + const currentLength = inputEl.value.length; + const maxLength = parseInt(characterCountEl.getAttribute("data-maxlength"), 10); + const statusMessage = characterCountEl.querySelector(STATUS_MESSAGE); + const srStatusMessage = characterCountEl.querySelector(STATUS_MESSAGE_SR_ONLY); + const currentStatusMessage = getCountMessage(currentLength, maxLength); + if (!maxLength) return; + const isOverLimit = currentLength && currentLength > maxLength; + statusMessage.textContent = currentStatusMessage; + srUpdateStatus(srStatusMessage, currentStatusMessage); + if (isOverLimit && !inputEl.validationMessage) { + inputEl.setCustomValidity(VALIDATION_MESSAGE); + } + if (!isOverLimit && inputEl.validationMessage === VALIDATION_MESSAGE) { + inputEl.setCustomValidity(""); + } + statusMessage.classList.toggle(MESSAGE_INVALID_CLASS, isOverLimit); }; +/** + * Initialize component + * + * @description On init this function will create elements and update any + * attributes so it can tell the user how many characters are left. + * @param {HTMLInputElement|HTMLTextAreaElement} inputEl the components input + */ +const enhanceCharacterCount = inputEl => { + const { + characterCountEl, + messageEl + } = getCharacterCountElements(inputEl); + + // Hide hint and remove aria-live for backwards compatibility + messageEl.classList.add("usa-sr-only"); + messageEl.removeAttribute("aria-live"); + setDataLength(inputEl); + createStatusMessages(characterCountEl); +}; const characterCount = behavior({ input: { [INPUT]() { updateCountMessage(this); } - } }, { init(root) { - select(INPUT, root).forEach(input => { - setupAttributes(input); - updateCountMessage(input); - }); + select(INPUT, root).forEach(input => enhanceCharacterCount(input)); }, - MESSAGE_INVALID_CLASS, - VALIDATION_MESSAGE + VALIDATION_MESSAGE, + STATUS_MESSAGE_CLASS, + STATUS_MESSAGE_SR_ONLY_CLASS, + DEFAULT_STATUS_LABEL, + createStatusMessages, + getCountMessage, + updateCountMessage }); module.exports = characterCount; -},{"../../uswds-core/src/js/config":33,"../../uswds-core/src/js/utils/behavior":43,"../../uswds-core/src/js/utils/select":50}],19:[function(require,module,exports){ +},{"../../uswds-core/src/js/config":36,"../../uswds-core/src/js/utils/behavior":46,"../../uswds-core/src/js/utils/debounce":47,"../../uswds-core/src/js/utils/select":54}],19:[function(require,module,exports){ "use strict"; const keymap = require("receptor/keymap"); - const selectOrMatches = require("../../uswds-core/src/js/utils/select-or-matches"); - const behavior = require("../../uswds-core/src/js/utils/behavior"); - const Sanitizer = require("../../uswds-core/src/js/utils/sanitizer"); - const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - const { CLICK } = require("../../uswds-core/src/js/events"); - const COMBO_BOX_CLASS = `${PREFIX}-combo-box`; const COMBO_BOX_PRISTINE_CLASS = `${COMBO_BOX_CLASS}--pristine`; const SELECT_CLASS = `${COMBO_BOX_CLASS}__select`; @@ -1094,18 +1048,15 @@ const LIST_OPTION_FOCUSED = `.${LIST_OPTION_FOCUSED_CLASS}`; const LIST_OPTION_SELECTED = `.${LIST_OPTION_SELECTED_CLASS}`; const STATUS = `.${STATUS_CLASS}`; const DEFAULT_FILTER = ".*{{query}}.*"; - const noop = () => {}; + /** * set the value of the element and dispatch a change event * * @param {HTMLInputElement|HTMLSelectElement} el The element to update * @param {string} value The new value of the element */ - - -const changeElementValue = function (el) { - let value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; +const changeElementValue = (el, value = "") => { const elementToChange = el; elementToChange.value = value; const event = new CustomEvent("change", { @@ -1117,6 +1068,7 @@ const changeElementValue = function (el) { }); elementToChange.dispatchEvent(event); }; + /** * The elements within the combo box. * @typedef {Object} ComboBoxContext @@ -1140,15 +1092,11 @@ const changeElementValue = function (el) { * @param {HTMLElement} el the element within the combo box * @returns {ComboBoxContext} elements */ - - const getComboBoxContext = el => { const comboBoxEl = el.closest(COMBO_BOX); - if (!comboBoxEl) { throw new Error(`Element is missing outer ${COMBO_BOX}`); } - const selectEl = comboBoxEl.querySelector(SELECT); const inputEl = comboBoxEl.querySelector(INPUT); const listEl = comboBoxEl.querySelector(LIST); @@ -1173,13 +1121,12 @@ const getComboBoxContext = el => { disableFiltering }; }; + /** * Disable the combo-box component * * @param {HTMLInputElement} el An element within the combo box component */ - - const disable = el => { const { inputEl, @@ -1191,13 +1138,29 @@ const disable = el => { toggleListBtnEl.disabled = true; inputEl.disabled = true; }; + +/** + * Check for aria-disabled on initialization + * + * @param {HTMLInputElement} el An element within the combo box component + */ +const ariaDisable = el => { + const { + inputEl, + toggleListBtnEl, + clearInputBtnEl + } = getComboBoxContext(el); + clearInputBtnEl.hidden = true; + clearInputBtnEl.setAttribute("aria-disabled", true); + toggleListBtnEl.setAttribute("aria-disabled", true); + inputEl.setAttribute("aria-disabled", true); +}; + /** * Enable the combo-box component * * @param {HTMLInputElement} el An element within the combo box component */ - - const enable = el => { const { inputEl, @@ -1209,23 +1172,19 @@ const enable = el => { toggleListBtnEl.disabled = false; inputEl.disabled = false; }; + /** * Enhance a select element into a combo box component. * * @param {HTMLElement} _comboBoxEl The initial element of the combo box component */ - - const enhanceComboBox = _comboBoxEl => { const comboBoxEl = _comboBoxEl.closest(COMBO_BOX); - if (comboBoxEl.dataset.enhanced) return; const selectEl = comboBoxEl.querySelector("select"); - if (!selectEl) { throw new Error(`${COMBO_BOX} is missing inner select`); } - const selectId = selectEl.id; const selectLabel = document.querySelector(`label[for="${selectId}"]`); const listId = `${selectId}--list`; @@ -1239,35 +1198,30 @@ const enhanceComboBox = _comboBoxEl => { placeholder } = comboBoxEl.dataset; let selectedOption; - if (placeholder) { additionalAttributes.push({ placeholder }); } - if (defaultValue) { for (let i = 0, len = selectEl.options.length; i < len; i += 1) { const optionEl = selectEl.options[i]; - if (optionEl.value === defaultValue) { selectedOption = optionEl; break; } } } + /** * Throw error if combobox is missing a label or label is missing * `for` attribute. Otherwise, set the ID to match the
    aria-labelledby */ - - if (!selectLabel || !selectLabel.matches(`label[for="${selectId}"]`)) { throw new Error(`${COMBO_BOX} for ${selectId} is either missing a label or a "for" attribute`); } else { selectLabel.setAttribute("id", listIdLabel); } - selectLabel.setAttribute("id", listIdLabel); selectEl.setAttribute("aria-hidden", "true"); selectEl.setAttribute("tabindex", "-1"); @@ -1282,8 +1236,9 @@ const enhanceComboBox = _comboBoxEl => { }); selectEl.removeAttribute(name); } - }); // sanitize doesn't like functions in template literals + }); + // sanitize doesn't like functions in template literals const input = document.createElement("input"); input.setAttribute("id", selectId); input.setAttribute("aria-owns", listId); @@ -1322,7 +1277,6 @@ const enhanceComboBox = _comboBoxEl => { When autocomplete results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures. `); - if (selectedOption) { const { inputEl @@ -1331,14 +1285,17 @@ const enhanceComboBox = _comboBoxEl => { changeElementValue(inputEl, selectedOption.text); comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS); } - if (selectEl.disabled) { disable(comboBoxEl); selectEl.disabled = false; } - + if (selectEl.hasAttribute("aria-disabled")) { + ariaDisable(comboBoxEl); + selectEl.removeAttribute("aria-disabled"); + } comboBoxEl.dataset.enhanced = "true"; }; + /** * Manage the focused element within the list options when * navigating via keyboard. @@ -1349,42 +1306,33 @@ const enhanceComboBox = _comboBoxEl => { * @param {boolean} options.skipFocus skip focus of highlighted item * @param {boolean} options.preventScroll should skip procedure to scroll to element */ - - -const highlightOption = function (el, nextEl) { - let { - skipFocus, - preventScroll - } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; +const highlightOption = (el, nextEl, { + skipFocus, + preventScroll +} = {}) => { const { inputEl, listEl, focusedOptionEl } = getComboBoxContext(el); - if (focusedOptionEl) { focusedOptionEl.classList.remove(LIST_OPTION_FOCUSED_CLASS); focusedOptionEl.setAttribute("tabIndex", "-1"); } - if (nextEl) { inputEl.setAttribute("aria-activedescendant", nextEl.id); nextEl.setAttribute("tabIndex", "0"); nextEl.classList.add(LIST_OPTION_FOCUSED_CLASS); - if (!preventScroll) { const optionBottom = nextEl.offsetTop + nextEl.offsetHeight; const currentBottom = listEl.scrollTop + listEl.offsetHeight; - if (optionBottom > currentBottom) { listEl.scrollTop = optionBottom - listEl.offsetHeight; } - if (nextEl.offsetTop < listEl.scrollTop) { listEl.scrollTop = nextEl.offsetTop; } } - if (!skipFocus) { nextEl.focus({ preventScroll @@ -1395,6 +1343,7 @@ const highlightOption = function (el, nextEl) { inputEl.focus(); } }; + /** * Generate a dynamic regular expression based off of a replaceable and possibly filtered value. * @@ -1402,41 +1351,30 @@ const highlightOption = function (el, nextEl) { * @param {string} query The value to use in the regular expression * @param {object} extras An object of regular expressions to replace and filter the query */ - - -const generateDynamicRegExp = function (filter) { - let query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; - let extras = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - +const generateDynamicRegExp = (filter, query = "", extras = {}) => { const escapeRegExp = text => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - let find = filter.replace(/{{(.*?)}}/g, (m, $1) => { const key = $1.trim(); const queryFilter = extras[key]; - if (key !== "query" && queryFilter) { const matcher = new RegExp(queryFilter, "i"); const matches = query.match(matcher); - if (matches) { return escapeRegExp(matches[1]); } - return ""; } - return escapeRegExp(query); }); find = `^(?:${find})$`; return new RegExp(find, "i"); }; + /** * Display the option list of a combo box component. * * @param {HTMLElement} el An element within the combo box component */ - - const displayList = el => { const { comboBoxEl, @@ -1454,42 +1392,34 @@ const displayList = el => { const filter = comboBoxEl.dataset.filter || DEFAULT_FILTER; const regex = generateDynamicRegExp(filter, inputValue, comboBoxEl.dataset); const options = []; - for (let i = 0, len = selectEl.options.length; i < len; i += 1) { const optionEl = selectEl.options[i]; const optionId = `${listOptionBaseId}${options.length}`; - if (optionEl.value && (disableFiltering || isPristine || !inputValue || regex.test(optionEl.text))) { if (selectEl.value && optionEl.value === selectEl.value) { selectedItemId = optionId; } - if (disableFiltering && !firstFoundId && regex.test(optionEl.text)) { firstFoundId = optionId; } - options.push(optionEl); } } - const numOptions = options.length; const optionHtml = options.map((option, index) => { const optionId = `${listOptionBaseId}${index}`; const classes = [LIST_OPTION_CLASS]; let tabindex = "-1"; let ariaSelected = "false"; - if (optionId === selectedItemId) { classes.push(LIST_OPTION_SELECTED_CLASS, LIST_OPTION_FOCUSED_CLASS); tabindex = "0"; ariaSelected = "true"; } - if (!selectedItemId && index === 0) { classes.push(LIST_OPTION_FOCUSED_CLASS); tabindex = "0"; } - const li = document.createElement("li"); li.setAttribute("aria-setsize", options.length); li.setAttribute("aria-posinset", index + 1); @@ -1506,7 +1436,6 @@ const displayList = el => { noResults.setAttribute("class", `${LIST_OPTION_CLASS}--no-results`); noResults.textContent = "No results found"; listEl.hidden = false; - if (numOptions) { listEl.innerHTML = ""; optionHtml.forEach(item => listEl.insertAdjacentElement("beforeend", item)); @@ -1514,30 +1443,26 @@ const displayList = el => { listEl.innerHTML = ""; listEl.insertAdjacentElement("beforeend", noResults); } - inputEl.setAttribute("aria-expanded", "true"); statusEl.textContent = numOptions ? `${numOptions} result${numOptions > 1 ? "s" : ""} available.` : "No results."; let itemToFocus; - if (isPristine && selectedItemId) { itemToFocus = listEl.querySelector(`#${selectedItemId}`); } else if (disableFiltering && firstFoundId) { itemToFocus = listEl.querySelector(`#${firstFoundId}`); } - if (itemToFocus) { highlightOption(listEl, itemToFocus, { skipFocus: true }); } }; + /** * Hide the option list of a combo box component. * * @param {HTMLElement} el An element within the combo box component */ - - const hideList = el => { const { inputEl, @@ -1548,21 +1473,18 @@ const hideList = el => { statusEl.innerHTML = ""; inputEl.setAttribute("aria-expanded", "false"); inputEl.setAttribute("aria-activedescendant", ""); - if (focusedOptionEl) { focusedOptionEl.classList.remove(LIST_OPTION_FOCUSED_CLASS); } - listEl.scrollTop = 0; listEl.hidden = true; }; + /** * Select an option list of the combo box component. * * @param {HTMLElement} listOptionEl The list option being selected */ - - const selectItem = listOptionEl => { const { comboBoxEl, @@ -1575,13 +1497,12 @@ const selectItem = listOptionEl => { hideList(comboBoxEl); inputEl.focus(); }; + /** * Clear the input of the combo box * * @param {HTMLButtonElement} clearButtonEl The clear input button */ - - const clearInput = clearButtonEl => { const { comboBoxEl, @@ -1596,13 +1517,12 @@ const clearInput = clearButtonEl => { if (listShown) displayList(comboBoxEl); inputEl.focus(); }; + /** * Reset the select based off of currently set select value * * @param {HTMLElement} el An element within the combo box component */ - - const resetSelection = el => { const { comboBoxEl, @@ -1611,26 +1531,23 @@ const resetSelection = el => { } = getComboBoxContext(el); const selectValue = selectEl.value; const inputValue = (inputEl.value || "").toLowerCase(); - if (selectValue) { for (let i = 0, len = selectEl.options.length; i < len; i += 1) { const optionEl = selectEl.options[i]; - if (optionEl.value === selectValue) { if (inputValue !== optionEl.text) { changeElementValue(inputEl, optionEl.text); } - comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS); return; } } } - if (inputValue) { changeElementValue(inputEl); } }; + /** * Select an option list of the combo box component based off of * having a current focused list option or @@ -1639,8 +1556,6 @@ const resetSelection = el => { * * @param {HTMLElement} el An element within the combo box component */ - - const completeSelection = el => { const { comboBoxEl, @@ -1650,11 +1565,9 @@ const completeSelection = el => { } = getComboBoxContext(el); statusEl.textContent = ""; const inputValue = (inputEl.value || "").toLowerCase(); - if (inputValue) { for (let i = 0, len = selectEl.options.length; i < len; i += 1) { const optionEl = selectEl.options[i]; - if (optionEl.text.toLowerCase() === inputValue) { changeElementValue(selectEl, optionEl.value); changeElementValue(inputEl, optionEl.text); @@ -1663,16 +1576,14 @@ const completeSelection = el => { } } } - resetSelection(comboBoxEl); }; + /** * Handle the escape event within the combo box component. * * @param {KeyboardEvent} event An event within the combo box component */ - - const handleEscape = event => { const { comboBoxEl, @@ -1682,38 +1593,32 @@ const handleEscape = event => { resetSelection(comboBoxEl); inputEl.focus(); }; + /** * Handle the down event within the combo box component. * * @param {KeyboardEvent} event An event within the combo box component */ - - const handleDownFromInput = event => { const { comboBoxEl, listEl } = getComboBoxContext(event.target); - if (listEl.hidden) { displayList(comboBoxEl); } - const nextOptionEl = listEl.querySelector(LIST_OPTION_FOCUSED) || listEl.querySelector(LIST_OPTION); - if (nextOptionEl) { highlightOption(comboBoxEl, nextOptionEl); } - event.preventDefault(); }; + /** * Handle the enter event from an input element within the combo box component. * * @param {KeyboardEvent} event An event within the combo box component */ - - const handleEnterFromInput = event => { const { comboBoxEl, @@ -1721,59 +1626,51 @@ const handleEnterFromInput = event => { } = getComboBoxContext(event.target); const listShown = !listEl.hidden; completeSelection(comboBoxEl); - if (listShown) { hideList(comboBoxEl); } - event.preventDefault(); }; + /** * Handle the down event within the combo box component. * * @param {KeyboardEvent} event An event within the combo box component */ - - const handleDownFromListOption = event => { const focusedOptionEl = event.target; const nextOptionEl = focusedOptionEl.nextSibling; - if (nextOptionEl) { highlightOption(focusedOptionEl, nextOptionEl); } - event.preventDefault(); }; + /** - * Handle the tab event from an list option element within the combo box component. + * Handle the space event from an list option element within the combo box component. * * @param {KeyboardEvent} event An event within the combo box component */ - - -const handleTabFromListOption = event => { +const handleSpaceFromListOption = event => { selectItem(event.target); event.preventDefault(); }; + /** * Handle the enter event from list option within the combo box component. * * @param {KeyboardEvent} event An event within the combo box component */ - - const handleEnterFromListOption = event => { selectItem(event.target); event.preventDefault(); }; + /** * Handle the up event from list option within the combo box component. * * @param {KeyboardEvent} event An event within the combo box component */ - - const handleUpFromListOption = event => { const { comboBoxEl, @@ -1783,23 +1680,20 @@ const handleUpFromListOption = event => { const nextOptionEl = focusedOptionEl && focusedOptionEl.previousSibling; const listShown = !listEl.hidden; highlightOption(comboBoxEl, nextOptionEl); - if (listShown) { event.preventDefault(); } - if (!nextOptionEl) { hideList(comboBoxEl); } }; + /** * Select list option on the mouseover event. * * @param {MouseEvent} event The mouseover event * @param {HTMLLIElement} listOptionEl An element within the combo box component */ - - const handleMouseover = listOptionEl => { const isCurrentlyFocused = listOptionEl.classList.contains(LIST_OPTION_FOCUSED_CLASS); if (isCurrentlyFocused) return; @@ -1807,68 +1701,58 @@ const handleMouseover = listOptionEl => { preventScroll: true }); }; + /** * Toggle the list when the button is clicked * * @param {HTMLElement} el An element within the combo box component */ - - const toggleList = el => { const { comboBoxEl, listEl, inputEl } = getComboBoxContext(el); - if (listEl.hidden) { displayList(comboBoxEl); } else { hideList(comboBoxEl); } - inputEl.focus(); }; + /** * Handle click from input * * @param {HTMLInputElement} el An element within the combo box component */ - - const handleClickFromInput = el => { const { comboBoxEl, listEl } = getComboBoxContext(el); - if (listEl.hidden) { displayList(comboBoxEl); } }; - const comboBox = behavior({ [CLICK]: { [INPUT]() { if (this.disabled) return; handleClickFromInput(this); }, - [TOGGLE_LIST_BUTTON]() { if (this.disabled) return; toggleList(this); }, - [LIST_OPTION]() { if (this.disabled) return; selectItem(this); }, - [CLEAR_INPUT_BUTTON]() { if (this.disabled) return; clearInput(this); } - }, focusout: { [COMBO_BOX](event) { @@ -1877,7 +1761,6 @@ const comboBox = behavior({ hideList(this); } } - }, keydown: { [COMBO_BOX]: keymap({ @@ -1894,7 +1777,7 @@ const comboBox = behavior({ ArrowDown: handleDownFromListOption, Down: handleDownFromListOption, Enter: handleEnterFromListOption, - Tab: handleTabFromListOption, + " ": handleSpaceFromListOption, "Shift+Tab": noop }) }, @@ -1904,13 +1787,11 @@ const comboBox = behavior({ comboBoxEl.classList.remove(COMBO_BOX_PRISTINE_CLASS); displayList(this); } - }, mouseover: { [LIST_OPTION]() { handleMouseover(this); } - } }, { init(root) { @@ -1918,7 +1799,6 @@ const comboBox = behavior({ enhanceComboBox(comboBoxEl); }); }, - getComboBoxContext, enhanceComboBox, generateDynamicRegExp, @@ -1930,31 +1810,22 @@ const comboBox = behavior({ }); module.exports = comboBox; -},{"../../uswds-core/src/js/config":33,"../../uswds-core/src/js/events":34,"../../uswds-core/src/js/utils/behavior":43,"../../uswds-core/src/js/utils/sanitizer":47,"../../uswds-core/src/js/utils/select-or-matches":49,"receptor/keymap":12}],20:[function(require,module,exports){ +},{"../../uswds-core/src/js/config":36,"../../uswds-core/src/js/events":37,"../../uswds-core/src/js/utils/behavior":46,"../../uswds-core/src/js/utils/sanitizer":51,"../../uswds-core/src/js/utils/select-or-matches":53,"receptor/keymap":11}],20:[function(require,module,exports){ "use strict"; const keymap = require("receptor/keymap"); - const behavior = require("../../uswds-core/src/js/utils/behavior"); - const select = require("../../uswds-core/src/js/utils/select"); - const selectOrMatches = require("../../uswds-core/src/js/utils/select-or-matches"); - const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - const { CLICK } = require("../../uswds-core/src/js/events"); - const activeElement = require("../../uswds-core/src/js/utils/active-element"); - const isIosDevice = require("../../uswds-core/src/js/utils/is-ios-device"); - const Sanitizer = require("../../uswds-core/src/js/utils/sanitizer"); - const DATE_PICKER_CLASS = `${PREFIX}-date-picker`; const DATE_PICKER_WRAPPER_CLASS = `${DATE_PICKER_CLASS}__wrapper`; const DATE_PICKER_INITIALIZED_CLASS = `${DATE_PICKER_CLASS}--initialized`; @@ -2031,18 +1902,12 @@ const DEFAULT_MIN_DATE = "0000-01-01"; const DEFAULT_EXTERNAL_DATE_FORMAT = "MM/DD/YYYY"; const INTERNAL_DATE_FORMAT = "YYYY-MM-DD"; const NOT_DISABLED_SELECTOR = ":not([disabled])"; - -const processFocusableSelectors = function () { - for (var _len = arguments.length, selectors = new Array(_len), _key = 0; _key < _len; _key++) { - selectors[_key] = arguments[_key]; - } - - return selectors.map(query => query + NOT_DISABLED_SELECTOR).join(", "); -}; - +const processFocusableSelectors = (...selectors) => selectors.map(query => query + NOT_DISABLED_SELECTOR).join(", "); const DATE_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_PREVIOUS_YEAR, CALENDAR_PREVIOUS_MONTH, CALENDAR_YEAR_SELECTION, CALENDAR_MONTH_SELECTION, CALENDAR_NEXT_YEAR, CALENDAR_NEXT_MONTH, CALENDAR_DATE_FOCUSED); const MONTH_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_MONTH_FOCUSED); -const YEAR_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_PREVIOUS_YEAR_CHUNK, CALENDAR_NEXT_YEAR_CHUNK, CALENDAR_YEAR_FOCUSED); // #region Date Manipulation Functions +const YEAR_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_PREVIOUS_YEAR_CHUNK, CALENDAR_NEXT_YEAR_CHUNK, CALENDAR_YEAR_FOCUSED); + +// #region Date Manipulation Functions /** * Keep date within month. Month would only be over by 1 to 3 days @@ -2051,14 +1916,13 @@ const YEAR_PICKER_FOCUSABLE = processFocusableSelectors(CALENDAR_PREVIOUS_YEAR_C * @param {number} month the correct month * @returns {Date} the date, corrected if needed */ - const keepDateWithinMonth = (dateToCheck, month) => { if (month !== dateToCheck.getMonth()) { dateToCheck.setDate(0); } - return dateToCheck; }; + /** * Set date from month day year * @@ -2067,20 +1931,17 @@ const keepDateWithinMonth = (dateToCheck, month) => { * @param {number} date the date to set * @returns {Date} the set date */ - - const setDate = (year, month, date) => { const newDate = new Date(0); newDate.setFullYear(year, month, date); return newDate; }; + /** * todays date * * @returns {Date} todays date */ - - const today = () => { const newDate = new Date(); const day = newDate.getDate(); @@ -2088,32 +1949,31 @@ const today = () => { const year = newDate.getFullYear(); return setDate(year, month, day); }; + /** * Set date to first day of the month * * @param {number} date the date to adjust * @returns {Date} the adjusted date */ - - const startOfMonth = date => { const newDate = new Date(0); newDate.setFullYear(date.getFullYear(), date.getMonth(), 1); return newDate; }; + /** * Set date to last day of the month * * @param {number} date the date to adjust * @returns {Date} the adjusted date */ - - const lastDayOfMonth = date => { const newDate = new Date(0); newDate.setFullYear(date.getFullYear(), date.getMonth() + 1, 0); return newDate; }; + /** * Add days to date * @@ -2121,13 +1981,12 @@ const lastDayOfMonth = date => { * @param {number} numDays the difference in days * @returns {Date} the adjusted date */ - - const addDays = (_date, numDays) => { const newDate = new Date(_date.getTime()); newDate.setDate(newDate.getDate() + numDays); return newDate; }; + /** * Subtract days from date * @@ -2135,9 +1994,8 @@ const addDays = (_date, numDays) => { * @param {number} numDays the difference in days * @returns {Date} the adjusted date */ - - const subDays = (_date, numDays) => addDays(_date, -numDays); + /** * Add weeks to date * @@ -2145,9 +2003,8 @@ const subDays = (_date, numDays) => addDays(_date, -numDays); * @param {number} numWeeks the difference in weeks * @returns {Date} the adjusted date */ - - const addWeeks = (_date, numWeeks) => addDays(_date, numWeeks * 7); + /** * Subtract weeks from date * @@ -2155,22 +2012,19 @@ const addWeeks = (_date, numWeeks) => addDays(_date, numWeeks * 7); * @param {number} numWeeks the difference in weeks * @returns {Date} the adjusted date */ - - const subWeeks = (_date, numWeeks) => addWeeks(_date, -numWeeks); + /** * Set date to the start of the week (Sunday) * * @param {Date} _date the date to adjust * @returns {Date} the adjusted date */ - - const startOfWeek = _date => { const dayOfWeek = _date.getDay(); - return subDays(_date, dayOfWeek); }; + /** * Set date to the end of the week (Saturday) * @@ -2178,13 +2032,11 @@ const startOfWeek = _date => { * @param {number} numWeeks the difference in weeks * @returns {Date} the adjusted date */ - - const endOfWeek = _date => { const dayOfWeek = _date.getDay(); - return addDays(_date, 6 - dayOfWeek); }; + /** * Add months to date and keep date within month * @@ -2192,8 +2044,6 @@ const endOfWeek = _date => { * @param {number} numMonths the difference in months * @returns {Date} the adjusted date */ - - const addMonths = (_date, numMonths) => { const newDate = new Date(_date.getTime()); const dateMonth = (newDate.getMonth() + 12 + numMonths) % 12; @@ -2201,6 +2051,7 @@ const addMonths = (_date, numMonths) => { keepDateWithinMonth(newDate, dateMonth); return newDate; }; + /** * Subtract months from date * @@ -2208,9 +2059,8 @@ const addMonths = (_date, numMonths) => { * @param {number} numMonths the difference in months * @returns {Date} the adjusted date */ - - const subMonths = (_date, numMonths) => addMonths(_date, -numMonths); + /** * Add years to date and keep date within month * @@ -2218,9 +2068,8 @@ const subMonths = (_date, numMonths) => addMonths(_date, -numMonths); * @param {number} numYears the difference in years * @returns {Date} the adjusted date */ - - const addYears = (_date, numYears) => addMonths(_date, numYears * 12); + /** * Subtract years from date * @@ -2228,9 +2077,8 @@ const addYears = (_date, numYears) => addMonths(_date, numYears * 12); * @param {number} numYears the difference in years * @returns {Date} the adjusted date */ - - const subYears = (_date, numYears) => addYears(_date, -numYears); + /** * Set months of date * @@ -2238,14 +2086,13 @@ const subYears = (_date, numYears) => addYears(_date, -numYears); * @param {number} month zero-indexed month to set * @returns {Date} the adjusted date */ - - const setMonth = (_date, month) => { const newDate = new Date(_date.getTime()); newDate.setMonth(month); keepDateWithinMonth(newDate, month); return newDate; }; + /** * Set year of date * @@ -2253,8 +2100,6 @@ const setMonth = (_date, month) => { * @param {number} year the year to set * @returns {Date} the adjusted date */ - - const setYear = (_date, year) => { const newDate = new Date(_date.getTime()); const month = newDate.getMonth(); @@ -2262,6 +2107,7 @@ const setYear = (_date, year) => { keepDateWithinMonth(newDate, month); return newDate; }; + /** * Return the earliest date * @@ -2269,17 +2115,14 @@ const setYear = (_date, year) => { * @param {Date} dateB date to compare * @returns {Date} the earliest date */ - - const min = (dateA, dateB) => { let newDate = dateA; - if (dateB < dateA) { newDate = dateB; } - return new Date(newDate.getTime()); }; + /** * Return the latest date * @@ -2287,17 +2130,14 @@ const min = (dateA, dateB) => { * @param {Date} dateB date to compare * @returns {Date} the latest date */ - - const max = (dateA, dateB) => { let newDate = dateA; - if (dateB > dateA) { newDate = dateB; } - return new Date(newDate.getTime()); }; + /** * Check if dates are the in the same year * @@ -2305,9 +2145,8 @@ const max = (dateA, dateB) => { * @param {Date} dateB date to compare * @returns {boolean} are dates in the same year */ - - const isSameYear = (dateA, dateB) => dateA && dateB && dateA.getFullYear() === dateB.getFullYear(); + /** * Check if dates are the in the same month * @@ -2315,9 +2154,8 @@ const isSameYear = (dateA, dateB) => dateA && dateB && dateA.getFullYear() === d * @param {Date} dateB date to compare * @returns {boolean} are dates in the same month */ - - const isSameMonth = (dateA, dateB) => isSameYear(dateA, dateB) && dateA.getMonth() === dateB.getMonth(); + /** * Check if dates are the same date * @@ -2325,9 +2163,8 @@ const isSameMonth = (dateA, dateB) => isSameYear(dateA, dateB) && dateA.getMonth * @param {Date} dateA the date to compare * @returns {boolean} are dates the same date */ - - const isSameDay = (dateA, dateB) => isSameMonth(dateA, dateB) && dateA.getDate() === dateB.getDate(); + /** * return a new date within minimum and maximum date * @@ -2336,19 +2173,16 @@ const isSameDay = (dateA, dateB) => isSameMonth(dateA, dateB) && dateA.getDate() * @param {Date} maxDate maximum date to allow * @returns {Date} the date between min and max */ - - const keepDateBetweenMinAndMax = (date, minDate, maxDate) => { let newDate = date; - if (date < minDate) { newDate = minDate; } else if (maxDate && date > maxDate) { newDate = maxDate; } - return new Date(newDate.getTime()); }; + /** * Check if dates is valid. * @@ -2357,9 +2191,8 @@ const keepDateBetweenMinAndMax = (date, minDate, maxDate) => { * @param {Date} maxDate maximum date to allow * @return {boolean} is there a day within the month within min and max dates */ - - const isDateWithinMinAndMax = (date, minDate, maxDate) => date >= minDate && (!maxDate || date <= maxDate); + /** * Check if dates month is invalid. * @@ -2368,9 +2201,8 @@ const isDateWithinMinAndMax = (date, minDate, maxDate) => date >= minDate && (!m * @param {Date} maxDate maximum date to allow * @return {boolean} is the month outside min or max dates */ - - const isDatesMonthOutsideMinOrMax = (date, minDate, maxDate) => lastDayOfMonth(date) < minDate || maxDate && startOfMonth(date) > maxDate; + /** * Check if dates year is invalid. * @@ -2379,9 +2211,8 @@ const isDatesMonthOutsideMinOrMax = (date, minDate, maxDate) => lastDayOfMonth(d * @param {Date} maxDate maximum date to allow * @return {boolean} is the month outside min or max dates */ - - const isDatesYearOutsideMinOrMax = (date, minDate, maxDate) => lastDayOfMonth(setMonth(date, 11)) < minDate || maxDate && startOfMonth(setMonth(date, 0)) > maxDate; + /** * Parse a date with format M-D-YY * @@ -2390,37 +2221,27 @@ const isDatesYearOutsideMinOrMax = (date, minDate, maxDate) => lastDayOfMonth(se * @param {boolean} adjustDate should the date be adjusted * @returns {Date} the parsed date */ - - -const parseDateString = function (dateString) { - let dateFormat = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : INTERNAL_DATE_FORMAT; - let adjustDate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; +const parseDateString = (dateString, dateFormat = INTERNAL_DATE_FORMAT, adjustDate = false) => { let date; let month; let day; let year; let parsed; - if (dateString) { let monthStr; let dayStr; let yearStr; - if (dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT) { [monthStr, dayStr, yearStr] = dateString.split("/"); } else { [yearStr, monthStr, dayStr] = dateString.split("-"); } - if (yearStr) { parsed = parseInt(yearStr, 10); - if (!Number.isNaN(parsed)) { year = parsed; - if (adjustDate) { year = Math.max(0, year); - if (yearStr.length < 3) { const currentYear = today().getFullYear(); const currentYearStub = currentYear - currentYear % 10 ** yearStr.length; @@ -2429,26 +2250,20 @@ const parseDateString = function (dateString) { } } } - if (monthStr) { parsed = parseInt(monthStr, 10); - if (!Number.isNaN(parsed)) { month = parsed; - if (adjustDate) { month = Math.max(1, month); month = Math.min(12, month); } } } - if (month && dayStr && year != null) { parsed = parseInt(dayStr, 10); - if (!Number.isNaN(parsed)) { day = parsed; - if (adjustDate) { const lastDayOfTheMonth = setDate(year, month, 0).getDate(); day = Math.max(1, day); @@ -2456,14 +2271,13 @@ const parseDateString = function (dateString) { } } } - if (month && day && year != null) { date = setDate(year, month - 1, day); } } - return date; }; + /** * Format a date to format MM-DD-YYYY * @@ -2471,23 +2285,18 @@ const parseDateString = function (dateString) { * @param {string} dateFormat the format of the date string * @returns {string} the formatted date string */ - - -const formatDate = function (date) { - let dateFormat = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : INTERNAL_DATE_FORMAT; - +const formatDate = (date, dateFormat = INTERNAL_DATE_FORMAT) => { const padZeros = (value, length) => `0000${value}`.slice(-length); - const month = date.getMonth() + 1; const day = date.getDate(); const year = date.getFullYear(); - if (dateFormat === DEFAULT_EXTERNAL_DATE_FORMAT) { return [padZeros(month, 2), padZeros(day, 2), padZeros(year, 4)].join("/"); } - return [padZeros(year, 4), padZeros(month, 2), padZeros(day, 2)].join("-"); -}; // #endregion Date Manipulation Functions +}; + +// #endregion Date Manipulation Functions /** * Create a grid string from an array of html strings @@ -2496,33 +2305,26 @@ const formatDate = function (date) { * @param {number} rowSize the length of a row * @returns {string} the grid string */ - - const listToGridHtml = (htmlArray, rowSize) => { const grid = []; let row = []; let i = 0; - while (i < htmlArray.length) { row = []; const tr = document.createElement("tr"); - while (i < htmlArray.length && row.length < rowSize) { const td = document.createElement("td"); td.insertAdjacentElement("beforeend", htmlArray[i]); row.push(td); i += 1; } - row.forEach(element => { tr.insertAdjacentElement("beforeend", element); }); grid.push(tr); } - return grid; }; - const createTableBody = grid => { const tableBody = document.createElement("tbody"); grid.forEach(element => { @@ -2530,16 +2332,14 @@ const createTableBody = grid => { }); return tableBody; }; + /** * set the value of the element and dispatch a change event * * @param {HTMLInputElement} el The element to update * @param {string} value The new value of the element */ - - -const changeElementValue = function (el) { - let value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; +const changeElementValue = (el, value = "") => { const elementToChange = el; elementToChange.value = value; const event = new CustomEvent("change", { @@ -2551,6 +2351,7 @@ const changeElementValue = function (el) { }); elementToChange.dispatchEvent(event); }; + /** * The properties and elements within the date picker. * @typedef {Object} DatePickerContext @@ -2575,15 +2376,11 @@ const changeElementValue = function (el) { * @param {HTMLElement} el the element within the date picker * @returns {DatePickerContext} elements */ - - const getDatePickerContext = el => { const datePickerEl = el.closest(DATE_PICKER); - if (!datePickerEl) { throw new Error(`Element is missing outer ${DATE_PICKER}`); } - const internalInputEl = datePickerEl.querySelector(DATE_PICKER_INTERNAL_INPUT); const externalInputEl = datePickerEl.querySelector(DATE_PICKER_EXTERNAL_INPUT); const calendarEl = datePickerEl.querySelector(DATE_PICKER_CALENDAR); @@ -2597,11 +2394,9 @@ const getDatePickerContext = el => { const maxDate = parseDateString(datePickerEl.dataset.maxDate); const rangeDate = parseDateString(datePickerEl.dataset.rangeDate); const defaultDate = parseDateString(datePickerEl.dataset.defaultDate); - if (minDate && maxDate && minDate > maxDate) { throw new Error("Minimum date cannot be after maximum date"); } - return { calendarDate, minDate, @@ -2619,13 +2414,12 @@ const getDatePickerContext = el => { statusEl }; }; + /** * Disable the date picker component * * @param {HTMLElement} el An element within the date picker component */ - - const disable = el => { const { externalInputEl, @@ -2634,13 +2428,26 @@ const disable = el => { toggleBtnEl.disabled = true; externalInputEl.disabled = true; }; + +/** + * Check for aria-disabled on initialization + * + * @param {HTMLElement} el An element within the date picker component + */ +const ariaDisable = el => { + const { + externalInputEl, + toggleBtnEl + } = getDatePickerContext(el); + toggleBtnEl.setAttribute("aria-disabled", true); + externalInputEl.setAttribute("aria-disabled", true); +}; + /** * Enable the date picker component * * @param {HTMLElement} el An element within the date picker component */ - - const enable = el => { const { externalInputEl, @@ -2648,15 +2455,15 @@ const enable = el => { } = getDatePickerContext(el); toggleBtnEl.disabled = false; externalInputEl.disabled = false; -}; // #region Validation +}; + +// #region Validation /** * Validate the value in the input as a valid date of format M/D/YYYY * * @param {HTMLElement} el An element within the date picker component */ - - const isDateInputInvalid = el => { const { externalInputEl, @@ -2665,7 +2472,6 @@ const isDateInputInvalid = el => { } = getDatePickerContext(el); const dateString = externalInputEl.value; let isInvalid = false; - if (dateString) { isInvalid = true; const dateStringParts = dateString.split("/"); @@ -2675,73 +2481,63 @@ const isDateInputInvalid = el => { if (!Number.isNaN(parsed)) value = parsed; return value; }); - if (month && day && year != null) { const checkDate = setDate(year, month - 1, day); - if (checkDate.getMonth() === month - 1 && checkDate.getDate() === day && checkDate.getFullYear() === year && dateStringParts[2].length === 4 && isDateWithinMinAndMax(checkDate, minDate, maxDate)) { isInvalid = false; } } } - return isInvalid; }; + /** * Validate the value in the input as a valid date of format M/D/YYYY * * @param {HTMLElement} el An element within the date picker component */ - - const validateDateInput = el => { const { externalInputEl } = getDatePickerContext(el); const isInvalid = isDateInputInvalid(externalInputEl); - if (isInvalid && !externalInputEl.validationMessage) { externalInputEl.setCustomValidity(VALIDATION_MESSAGE); } - if (!isInvalid && externalInputEl.validationMessage === VALIDATION_MESSAGE) { externalInputEl.setCustomValidity(""); } -}; // #endregion Validation +}; + +// #endregion Validation /** * Enable the date picker component * * @param {HTMLElement} el An element within the date picker component */ - - const reconcileInputValues = el => { const { internalInputEl, inputDate } = getDatePickerContext(el); let newValue = ""; - if (inputDate && !isDateInputInvalid(el)) { newValue = formatDate(inputDate); } - if (internalInputEl.value !== newValue) { changeElementValue(internalInputEl, newValue); } }; + /** * Select the value of the date picker inputs. * * @param {HTMLButtonElement} el An element within the date picker component * @param {string} dateString The date string to update in YYYY-MM-DD format */ - - const setCalendarValue = (el, dateString) => { const parsedDate = parseDateString(dateString); - if (parsedDate) { const formattedDate = formatDate(parsedDate, DEFAULT_EXTERNAL_DATE_FORMAT); const { @@ -2754,36 +2550,30 @@ const setCalendarValue = (el, dateString) => { validateDateInput(datePickerEl); } }; + /** * Enhance an input with the date picker elements * * @param {HTMLElement} el The initial wrapping element of the date picker component */ - - const enhanceDatePicker = el => { const datePickerEl = el.closest(DATE_PICKER); const { defaultValue } = datePickerEl.dataset; const internalInputEl = datePickerEl.querySelector(`input`); - if (!internalInputEl) { throw new Error(`${DATE_PICKER} is missing inner input`); } - if (internalInputEl.value) { internalInputEl.value = ""; } - const minDate = parseDateString(datePickerEl.dataset.minDate || internalInputEl.getAttribute("min")); datePickerEl.dataset.minDate = minDate ? formatDate(minDate) : DEFAULT_MIN_DATE; const maxDate = parseDateString(datePickerEl.dataset.maxDate || internalInputEl.getAttribute("max")); - if (maxDate) { datePickerEl.dataset.maxDate = formatDate(maxDate); } - const calendarWrapper = document.createElement("div"); calendarWrapper.classList.add(DATE_PICKER_WRAPPER_CLASS); const externalInputEl = internalInputEl.cloneNode(); @@ -2792,7 +2582,7 @@ const enhanceDatePicker = el => { calendarWrapper.appendChild(externalInputEl); calendarWrapper.insertAdjacentHTML("beforeend", Sanitizer.escapeHTML` - +
    `); internalInputEl.setAttribute("aria-hidden", "true"); internalInputEl.setAttribute("tabindex", "-1"); @@ -2803,16 +2593,20 @@ const enhanceDatePicker = el => { internalInputEl.required = false; datePickerEl.appendChild(calendarWrapper); datePickerEl.classList.add(DATE_PICKER_INITIALIZED_CLASS); - if (defaultValue) { setCalendarValue(datePickerEl, defaultValue); } - if (internalInputEl.disabled) { disable(datePickerEl); internalInputEl.disabled = false; } -}; // #region Calendar - Date Selection View + if (internalInputEl.hasAttribute("aria-disabled")) { + ariaDisable(datePickerEl); + internalInputEl.removeAttribute("aria-disabled"); + } +}; + +// #region Calendar - Date Selection View /** * render the calendar. @@ -2821,8 +2615,6 @@ const enhanceDatePicker = el => { * @param {Date} _dateToDisplay a date to render on the calendar * @returns {HTMLElement} a reference to the new calendar element */ - - const renderCalendar = (el, _dateToDisplay) => { const { datePickerEl, @@ -2851,7 +2643,6 @@ const renderCalendar = (el, _dateToDisplay) => { const withinRangeStartDate = rangeDate && addDays(rangeStartDate, 1); const withinRangeEndDate = rangeDate && subDays(rangeEndDate, 1); const monthLabel = MONTH_LABELS[focusedMonth]; - const generateDateHtml = dateToRender => { const classes = [CALENDAR_DATE_CLASS]; const day = dateToRender.getDate(); @@ -2862,50 +2653,39 @@ const renderCalendar = (el, _dateToDisplay) => { let tabindex = "-1"; const isDisabled = !isDateWithinMinAndMax(dateToRender, minDate, maxDate); const isSelected = isSameDay(dateToRender, selectedDate); - if (isSameMonth(dateToRender, prevMonth)) { classes.push(CALENDAR_DATE_PREVIOUS_MONTH_CLASS); } - if (isSameMonth(dateToRender, focusedDate)) { classes.push(CALENDAR_DATE_CURRENT_MONTH_CLASS); } - if (isSameMonth(dateToRender, nextMonth)) { classes.push(CALENDAR_DATE_NEXT_MONTH_CLASS); } - if (isSelected) { classes.push(CALENDAR_DATE_SELECTED_CLASS); } - if (isSameDay(dateToRender, todaysDate)) { classes.push(CALENDAR_DATE_TODAY_CLASS); } - if (rangeDate) { if (isSameDay(dateToRender, rangeDate)) { classes.push(CALENDAR_DATE_RANGE_DATE_CLASS); } - if (isSameDay(dateToRender, rangeStartDate)) { classes.push(CALENDAR_DATE_RANGE_DATE_START_CLASS); } - if (isSameDay(dateToRender, rangeEndDate)) { classes.push(CALENDAR_DATE_RANGE_DATE_END_CLASS); } - if (isDateWithinMinAndMax(dateToRender, withinRangeStartDate, withinRangeEndDate)) { classes.push(CALENDAR_DATE_WITHIN_RANGE_CLASS); } } - if (isSameDay(dateToRender, focusedDate)) { tabindex = "0"; classes.push(CALENDAR_DATE_FOCUSED_CLASS); } - const monthStr = MONTH_LABELS[month]; const dayStr = DAY_OF_WEEK_LABELS[dayOfWeek]; const btn = document.createElement("button"); @@ -2918,24 +2698,20 @@ const renderCalendar = (el, _dateToDisplay) => { btn.setAttribute("data-value", formattedDate); btn.setAttribute("aria-label", Sanitizer.escapeHTML`${day} ${monthStr} ${year} ${dayStr}`); btn.setAttribute("aria-selected", isSelected ? "true" : "false"); - if (isDisabled === true) { btn.disabled = true; } - btn.textContent = day; return btn; - }; // set date to first rendered day - + }; + // set date to first rendered day dateToDisplay = startOfWeek(firstOfMonth); const days = []; - while (days.length < 28 || dateToDisplay.getMonth() === focusedMonth || days.length % 7 !== 0) { days.push(generateDateHtml(dateToDisplay)); dateToDisplay = addDays(dateToDisplay, 1); } - const datesGrid = listToGridHtml(days, 7); const newCalendar = calendarEl.cloneNode(); newCalendar.dataset.value = currentFormattedDate; @@ -2963,11 +2739,11 @@ const renderCalendar = (el, _dateToDisplay) => {
    @@ -2991,7 +2767,6 @@ const renderCalendar = (el, _dateToDisplay) => { `; const table = document.createElement("table"); table.setAttribute("class", CALENDAR_TABLE_CLASS); - table.setAttribute("role", "presentation"); const tableHead = document.createElement("thead"); table.insertAdjacentElement("beforeend", tableHead); const tableHeadRow = document.createElement("tr"); @@ -3008,41 +2783,38 @@ const renderCalendar = (el, _dateToDisplay) => { Object.keys(daysOfWeek).forEach(key => { const th = document.createElement("th"); th.setAttribute("class", CALENDAR_DAY_OF_WEEK_CLASS); - th.setAttribute("scope", "presentation"); + th.setAttribute("scope", "col"); th.setAttribute("aria-label", key); th.textContent = daysOfWeek[key]; tableHeadRow.insertAdjacentElement("beforeend", th); }); const tableBody = createTableBody(datesGrid); - table.insertAdjacentElement("beforeend", tableBody); // Container for Years, Months, and Days + table.insertAdjacentElement("beforeend", tableBody); + // Container for Years, Months, and Days const datePickerCalendarContainer = newCalendar.querySelector(CALENDAR_DATE_PICKER); datePickerCalendarContainer.insertAdjacentElement("beforeend", table); calendarEl.parentNode.replaceChild(newCalendar, calendarEl); datePickerEl.classList.add(DATE_PICKER_ACTIVE_CLASS); const statuses = []; - if (isSameDay(selectedDate, focusedDate)) { statuses.push("Selected date"); } - if (calendarWasHidden) { statuses.push("You can navigate by day using left and right arrows", "Weeks by using up and down arrows", "Months by using page up and page down keys", "Years by using shift plus page up and shift plus page down", "Home and end keys navigate to the beginning and end of a week"); statusEl.textContent = ""; } else { statuses.push(`${monthLabel} ${focusedYear}`); } - statusEl.textContent = statuses.join(". "); return newCalendar; }; + /** * Navigate back one year and display the calendar. * * @param {HTMLButtonElement} _buttonEl An element within the date picker component */ - - const displayPreviousYear = _buttonEl => { if (_buttonEl.disabled) return; const { @@ -3055,20 +2827,17 @@ const displayPreviousYear = _buttonEl => { date = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = renderCalendar(calendarEl, date); let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_YEAR); - if (nextToFocus.disabled) { nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); } - nextToFocus.focus(); }; + /** * Navigate back one month and display the calendar. * * @param {HTMLButtonElement} _buttonEl An element within the date picker component */ - - const displayPreviousMonth = _buttonEl => { if (_buttonEl.disabled) return; const { @@ -3081,20 +2850,17 @@ const displayPreviousMonth = _buttonEl => { date = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = renderCalendar(calendarEl, date); let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_MONTH); - if (nextToFocus.disabled) { nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); } - nextToFocus.focus(); }; + /** * Navigate forward one month and display the calendar. * * @param {HTMLButtonElement} _buttonEl An element within the date picker component */ - - const displayNextMonth = _buttonEl => { if (_buttonEl.disabled) return; const { @@ -3107,20 +2873,17 @@ const displayNextMonth = _buttonEl => { date = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = renderCalendar(calendarEl, date); let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_MONTH); - if (nextToFocus.disabled) { nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); } - nextToFocus.focus(); }; + /** * Navigate forward one year and display the calendar. * * @param {HTMLButtonElement} _buttonEl An element within the date picker component */ - - const displayNextYear = _buttonEl => { if (_buttonEl.disabled) return; const { @@ -3133,20 +2896,17 @@ const displayNextYear = _buttonEl => { date = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = renderCalendar(calendarEl, date); let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_YEAR); - if (nextToFocus.disabled) { nextToFocus = newCalendar.querySelector(CALENDAR_DATE_PICKER); } - nextToFocus.focus(); }; + /** * Hide the calendar of a date picker component. * * @param {HTMLElement} el An element within the date picker component */ - - const hideCalendar = el => { const { datePickerEl, @@ -3157,13 +2917,12 @@ const hideCalendar = el => { calendarEl.hidden = true; statusEl.textContent = ""; }; + /** * Select a date within the date picker component. * * @param {HTMLButtonElement} calendarDateEl A date element within the date picker component */ - - const selectDate = calendarDateEl => { if (calendarDateEl.disabled) return; const { @@ -3174,13 +2933,12 @@ const selectDate = calendarDateEl => { hideCalendar(datePickerEl); externalInputEl.focus(); }; + /** * Toggle the calendar. * * @param {HTMLButtonElement} el An element within the date picker component */ - - const toggleCalendar = el => { if (el.disabled) return; const { @@ -3190,7 +2948,6 @@ const toggleCalendar = el => { maxDate, defaultDate } = getDatePickerContext(el); - if (calendarEl.hidden) { const dateToDisplay = keepDateBetweenMinAndMax(inputDate || defaultDate || today(), minDate, maxDate); const newCalendar = renderCalendar(calendarEl, dateToDisplay); @@ -3199,13 +2956,12 @@ const toggleCalendar = el => { hideCalendar(el); } }; + /** * Update the calendar when visible. * * @param {HTMLElement} el an element within the date picker */ - - const updateCalendarIfVisible = el => { const { calendarEl, @@ -3214,22 +2970,21 @@ const updateCalendarIfVisible = el => { maxDate } = getDatePickerContext(el); const calendarShown = !calendarEl.hidden; - if (calendarShown && inputDate) { const dateToDisplay = keepDateBetweenMinAndMax(inputDate, minDate, maxDate); renderCalendar(calendarEl, dateToDisplay); } -}; // #endregion Calendar - Date Selection View -// #region Calendar - Month Selection View +}; + +// #endregion Calendar - Date Selection View +// #region Calendar - Month Selection View /** * Display the month selection screen in the date picker. * * @param {HTMLButtonElement} el An element within the date picker component * @returns {HTMLElement} a reference to the new calendar element */ - - const displayMonthSelection = (el, monthToDisplay) => { const { calendarEl, @@ -3246,16 +3001,13 @@ const displayMonthSelection = (el, monthToDisplay) => { let tabindex = "-1"; const classes = [CALENDAR_MONTH_CLASS]; const isSelected = index === selectedMonth; - if (index === focusedMonth) { tabindex = "0"; classes.push(CALENDAR_MONTH_FOCUSED_CLASS); } - if (isSelected) { classes.push(CALENDAR_MONTH_SELECTED_CLASS); } - const btn = document.createElement("button"); btn.setAttribute("type", "button"); btn.setAttribute("tabindex", tabindex); @@ -3263,11 +3015,9 @@ const displayMonthSelection = (el, monthToDisplay) => { btn.setAttribute("data-value", index); btn.setAttribute("data-label", month); btn.setAttribute("aria-selected", isSelected ? "true" : "false"); - if (isDisabled === true) { btn.disabled = true; } - btn.textContent = month; return btn; }); @@ -3287,13 +3037,12 @@ const displayMonthSelection = (el, monthToDisplay) => { statusEl.textContent = "Select a month."; return newCalendar; }; + /** * Select a month in the date picker component. * * @param {HTMLButtonElement} monthEl An month element within the date picker component */ - - const selectMonth = monthEl => { if (monthEl.disabled) return; const { @@ -3307,7 +3056,10 @@ const selectMonth = monthEl => { date = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = renderCalendar(calendarEl, date); newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); -}; // #endregion Calendar - Month Selection View +}; + +// #endregion Calendar - Month Selection View + // #region Calendar - Year Selection View /** @@ -3317,8 +3069,6 @@ const selectMonth = monthEl => { * @param {number} yearToDisplay year to display in year selection * @returns {HTMLElement} a reference to the new calendar element */ - - const displayYearSelection = (el, yearToDisplay) => { const { calendarEl, @@ -3336,115 +3086,119 @@ const displayYearSelection = (el, yearToDisplay) => { const nextYearChunkDisabled = isDatesYearOutsideMinOrMax(setYear(calendarDate, yearToChunk + YEAR_CHUNK), minDate, maxDate); const years = []; let yearIndex = yearToChunk; - while (years.length < YEAR_CHUNK) { const isDisabled = isDatesYearOutsideMinOrMax(setYear(calendarDate, yearIndex), minDate, maxDate); let tabindex = "-1"; const classes = [CALENDAR_YEAR_CLASS]; const isSelected = yearIndex === selectedYear; - if (yearIndex === focusedYear) { tabindex = "0"; classes.push(CALENDAR_YEAR_FOCUSED_CLASS); } - if (isSelected) { classes.push(CALENDAR_YEAR_SELECTED_CLASS); } - const btn = document.createElement("button"); btn.setAttribute("type", "button"); btn.setAttribute("tabindex", tabindex); btn.setAttribute("class", classes.join(" ")); btn.setAttribute("data-value", yearIndex); btn.setAttribute("aria-selected", isSelected ? "true" : "false"); - if (isDisabled === true) { btn.disabled = true; } - btn.textContent = yearIndex; years.push(btn); yearIndex += 1; } + const newCalendar = calendarEl.cloneNode(); - const newCalendar = calendarEl.cloneNode(); // create the years calendar wrapper - + // create the years calendar wrapper const yearsCalendarWrapper = document.createElement("div"); yearsCalendarWrapper.setAttribute("tabindex", "-1"); - yearsCalendarWrapper.setAttribute("class", CALENDAR_YEAR_PICKER_CLASS); // create table parent + yearsCalendarWrapper.setAttribute("class", CALENDAR_YEAR_PICKER_CLASS); + // create table parent const yearsTableParent = document.createElement("table"); - yearsTableParent.setAttribute("role", "presentation"); - yearsTableParent.setAttribute("class", CALENDAR_TABLE_CLASS); // create table body and table row + yearsTableParent.setAttribute("class", CALENDAR_TABLE_CLASS); + // create table body and table row const yearsHTMLTableBody = document.createElement("tbody"); - const yearsHTMLTableBodyRow = document.createElement("tr"); // create previous button + const yearsHTMLTableBodyRow = document.createElement("tr"); + // create previous button const previousYearsBtn = document.createElement("button"); previousYearsBtn.setAttribute("type", "button"); previousYearsBtn.setAttribute("class", CALENDAR_PREVIOUS_YEAR_CHUNK_CLASS); previousYearsBtn.setAttribute("aria-label", `Navigate back ${YEAR_CHUNK} years`); - if (prevYearChunkDisabled === true) { previousYearsBtn.disabled = true; } + previousYearsBtn.innerHTML = Sanitizer.escapeHTML` `; - previousYearsBtn.innerHTML = Sanitizer.escapeHTML` `; // create next button - + // create next button const nextYearsBtn = document.createElement("button"); nextYearsBtn.setAttribute("type", "button"); nextYearsBtn.setAttribute("class", CALENDAR_NEXT_YEAR_CHUNK_CLASS); nextYearsBtn.setAttribute("aria-label", `Navigate forward ${YEAR_CHUNK} years`); - if (nextYearChunkDisabled === true) { nextYearsBtn.disabled = true; } + nextYearsBtn.innerHTML = Sanitizer.escapeHTML` `; - nextYearsBtn.innerHTML = Sanitizer.escapeHTML` `; // create the actual years table - + // create the actual years table const yearsTable = document.createElement("table"); yearsTable.setAttribute("class", CALENDAR_TABLE_CLASS); - yearsTable.setAttribute("role", "presentation"); // create the years child table + yearsTable.setAttribute("role", "presentation"); + // create the years child table const yearsGrid = listToGridHtml(years, 3); - const yearsTableBody = createTableBody(yearsGrid); // append the grid to the years child table + const yearsTableBody = createTableBody(yearsGrid); - yearsTable.insertAdjacentElement("beforeend", yearsTableBody); // create the prev button td and append the prev button + // append the grid to the years child table + yearsTable.insertAdjacentElement("beforeend", yearsTableBody); + // create the prev button td and append the prev button const yearsHTMLTableBodyDetailPrev = document.createElement("td"); - yearsHTMLTableBodyDetailPrev.insertAdjacentElement("beforeend", previousYearsBtn); // create the years td and append the years child table + yearsHTMLTableBodyDetailPrev.insertAdjacentElement("beforeend", previousYearsBtn); + // create the years td and append the years child table const yearsHTMLTableBodyYearsDetail = document.createElement("td"); yearsHTMLTableBodyYearsDetail.setAttribute("colspan", "3"); - yearsHTMLTableBodyYearsDetail.insertAdjacentElement("beforeend", yearsTable); // create the next button td and append the next button + yearsHTMLTableBodyYearsDetail.insertAdjacentElement("beforeend", yearsTable); + // create the next button td and append the next button const yearsHTMLTableBodyDetailNext = document.createElement("td"); - yearsHTMLTableBodyDetailNext.insertAdjacentElement("beforeend", nextYearsBtn); // append the three td to the years child table row + yearsHTMLTableBodyDetailNext.insertAdjacentElement("beforeend", nextYearsBtn); + // append the three td to the years child table row yearsHTMLTableBodyRow.insertAdjacentElement("beforeend", yearsHTMLTableBodyDetailPrev); yearsHTMLTableBodyRow.insertAdjacentElement("beforeend", yearsHTMLTableBodyYearsDetail); - yearsHTMLTableBodyRow.insertAdjacentElement("beforeend", yearsHTMLTableBodyDetailNext); // append the table row to the years child table body + yearsHTMLTableBodyRow.insertAdjacentElement("beforeend", yearsHTMLTableBodyDetailNext); - yearsHTMLTableBody.insertAdjacentElement("beforeend", yearsHTMLTableBodyRow); // append the years table body to the years parent table + // append the table row to the years child table body + yearsHTMLTableBody.insertAdjacentElement("beforeend", yearsHTMLTableBodyRow); - yearsTableParent.insertAdjacentElement("beforeend", yearsHTMLTableBody); // append the parent table to the calendar wrapper + // append the years table body to the years parent table + yearsTableParent.insertAdjacentElement("beforeend", yearsHTMLTableBody); - yearsCalendarWrapper.insertAdjacentElement("beforeend", yearsTableParent); // append the years calender to the new calendar + // append the parent table to the calendar wrapper + yearsCalendarWrapper.insertAdjacentElement("beforeend", yearsTableParent); - newCalendar.insertAdjacentElement("beforeend", yearsCalendarWrapper); // replace calendar + // append the years calender to the new calendar + newCalendar.insertAdjacentElement("beforeend", yearsCalendarWrapper); + // replace calendar calendarEl.parentNode.replaceChild(newCalendar, calendarEl); statusEl.textContent = Sanitizer.escapeHTML`Showing years ${yearToChunk} to ${yearToChunk + YEAR_CHUNK - 1}. Select a year.`; return newCalendar; }; + /** * Navigate back by years and display the year selection screen. * * @param {HTMLButtonElement} el An element within the date picker component */ - - const displayPreviousYearChunk = el => { if (el.disabled) return; const { @@ -3461,20 +3215,17 @@ const displayPreviousYearChunk = el => { const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = displayYearSelection(calendarEl, cappedDate.getFullYear()); let nextToFocus = newCalendar.querySelector(CALENDAR_PREVIOUS_YEAR_CHUNK); - if (nextToFocus.disabled) { nextToFocus = newCalendar.querySelector(CALENDAR_YEAR_PICKER); } - nextToFocus.focus(); }; + /** * Navigate forward by years and display the year selection screen. * * @param {HTMLButtonElement} el An element within the date picker component */ - - const displayNextYearChunk = el => { if (el.disabled) return; const { @@ -3491,20 +3242,17 @@ const displayNextYearChunk = el => { const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = displayYearSelection(calendarEl, cappedDate.getFullYear()); let nextToFocus = newCalendar.querySelector(CALENDAR_NEXT_YEAR_CHUNK); - if (nextToFocus.disabled) { nextToFocus = newCalendar.querySelector(CALENDAR_YEAR_PICKER); } - nextToFocus.focus(); }; + /** * Select a year in the date picker component. * * @param {HTMLButtonElement} yearEl A year element within the date picker component */ - - const selectYear = yearEl => { if (yearEl.disabled) return; const { @@ -3518,7 +3266,10 @@ const selectYear = yearEl => { date = keepDateBetweenMinAndMax(date, minDate, maxDate); const newCalendar = renderCalendar(calendarEl, date); newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); -}; // #endregion Calendar - Year Selection View +}; + +// #endregion Calendar - Year Selection View + // #region Calendar Event Handling /** @@ -3526,8 +3277,6 @@ const selectYear = yearEl => { * * @param {KeyboardEvent} event the keydown event */ - - const handleEscapeFromCalendar = event => { const { datePickerEl, @@ -3536,7 +3285,10 @@ const handleEscapeFromCalendar = event => { hideCalendar(datePickerEl); externalInputEl.focus(); event.preventDefault(); -}; // #endregion Calendar Event Handling +}; + +// #endregion Calendar Event Handling + // #region Calendar Date Event Handling /** @@ -3544,8 +3296,6 @@ const handleEscapeFromCalendar = event => { * * @param {function} adjustDateFn function that returns the adjusted date */ - - const adjustCalendar = adjustDateFn => event => { const { calendarEl, @@ -3555,92 +3305,89 @@ const adjustCalendar = adjustDateFn => event => { } = getDatePickerContext(event.target); const date = adjustDateFn(calendarDate); const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); - if (!isSameDay(calendarDate, cappedDate)) { const newCalendar = renderCalendar(calendarEl, cappedDate); newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); } - event.preventDefault(); }; + /** * Navigate back one week and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - - const handleUpFromDate = adjustCalendar(date => subWeeks(date, 1)); + /** * Navigate forward one week and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handleDownFromDate = adjustCalendar(date => addWeeks(date, 1)); + /** * Navigate back one day and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handleLeftFromDate = adjustCalendar(date => subDays(date, 1)); + /** * Navigate forward one day and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handleRightFromDate = adjustCalendar(date => addDays(date, 1)); + /** * Navigate to the start of the week and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handleHomeFromDate = adjustCalendar(date => startOfWeek(date)); + /** * Navigate to the end of the week and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handleEndFromDate = adjustCalendar(date => endOfWeek(date)); + /** * Navigate forward one month and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handlePageDownFromDate = adjustCalendar(date => addMonths(date, 1)); + /** * Navigate back one month and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handlePageUpFromDate = adjustCalendar(date => subMonths(date, 1)); + /** * Navigate forward one year and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handleShiftPageDownFromDate = adjustCalendar(date => addYears(date, 1)); + /** * Navigate back one year and display the calendar. * * @param {KeyboardEvent} event the keydown event */ - const handleShiftPageUpFromDate = adjustCalendar(date => subYears(date, 1)); + /** * display the calendar for the mouseover date. * * @param {MouseEvent} event The mouseover event * @param {HTMLButtonElement} dateEl A date element within the date picker component */ - const handleMouseoverFromDate = dateEl => { if (dateEl.disabled) return; const calendarEl = dateEl.closest(DATE_PICKER_CALENDAR); @@ -3650,7 +3397,10 @@ const handleMouseoverFromDate = dateEl => { const dateToDisplay = parseDateString(hoverDate); const newCalendar = renderCalendar(calendarEl, dateToDisplay); newCalendar.querySelector(CALENDAR_DATE_FOCUSED).focus(); -}; // #endregion Calendar Date Event Handling +}; + +// #endregion Calendar Date Event Handling + // #region Calendar Month Event Handling /** @@ -3658,8 +3408,6 @@ const handleMouseoverFromDate = dateEl => { * * @param {function} adjustMonthFn function that returns the adjusted month */ - - const adjustMonthSelectionScreen = adjustMonthFn => event => { const monthEl = event.target; const selectedMonth = parseInt(monthEl.dataset.value, 10); @@ -3674,85 +3422,85 @@ const adjustMonthSelectionScreen = adjustMonthFn => event => { adjustedMonth = Math.max(0, Math.min(11, adjustedMonth)); const date = setMonth(calendarDate, adjustedMonth); const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); - if (!isSameMonth(currentDate, cappedDate)) { const newCalendar = displayMonthSelection(calendarEl, cappedDate.getMonth()); newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus(); } - event.preventDefault(); }; + /** * Navigate back three months and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - - const handleUpFromMonth = adjustMonthSelectionScreen(month => month - 3); + /** * Navigate forward three months and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleDownFromMonth = adjustMonthSelectionScreen(month => month + 3); + /** * Navigate back one month and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleLeftFromMonth = adjustMonthSelectionScreen(month => month - 1); + /** * Navigate forward one month and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleRightFromMonth = adjustMonthSelectionScreen(month => month + 1); + /** * Navigate to the start of the row of months and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleHomeFromMonth = adjustMonthSelectionScreen(month => month - month % 3); + /** * Navigate to the end of the row of months and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleEndFromMonth = adjustMonthSelectionScreen(month => month + 2 - month % 3); + /** * Navigate to the last month (December) and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handlePageDownFromMonth = adjustMonthSelectionScreen(() => 11); + /** * Navigate to the first month (January) and display the month selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handlePageUpFromMonth = adjustMonthSelectionScreen(() => 0); + /** * update the focus on a month when the mouse moves. * * @param {MouseEvent} event The mouseover event * @param {HTMLButtonElement} monthEl A month element within the date picker component */ - const handleMouseoverFromMonth = monthEl => { if (monthEl.disabled) return; if (monthEl.classList.contains(CALENDAR_MONTH_FOCUSED_CLASS)) return; const focusMonth = parseInt(monthEl.dataset.value, 10); const newCalendar = displayMonthSelection(monthEl, focusMonth); newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus(); -}; // #endregion Calendar Month Event Handling +}; + +// #endregion Calendar Month Event Handling + // #region Calendar Year Event Handling /** @@ -3760,8 +3508,6 @@ const handleMouseoverFromMonth = monthEl => { * * @param {function} adjustYearFn function that returns the adjusted year */ - - const adjustYearSelectionScreen = adjustYearFn => event => { const yearEl = event.target; const selectedYear = parseInt(yearEl.dataset.value, 10); @@ -3776,87 +3522,86 @@ const adjustYearSelectionScreen = adjustYearFn => event => { adjustedYear = Math.max(0, adjustedYear); const date = setYear(calendarDate, adjustedYear); const cappedDate = keepDateBetweenMinAndMax(date, minDate, maxDate); - if (!isSameYear(currentDate, cappedDate)) { const newCalendar = displayYearSelection(calendarEl, cappedDate.getFullYear()); newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus(); } - event.preventDefault(); }; + /** * Navigate back three years and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - - const handleUpFromYear = adjustYearSelectionScreen(year => year - 3); + /** * Navigate forward three years and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleDownFromYear = adjustYearSelectionScreen(year => year + 3); + /** * Navigate back one year and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleLeftFromYear = adjustYearSelectionScreen(year => year - 1); + /** * Navigate forward one year and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleRightFromYear = adjustYearSelectionScreen(year => year + 1); + /** * Navigate to the start of the row of years and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleHomeFromYear = adjustYearSelectionScreen(year => year - year % 3); + /** * Navigate to the end of the row of years and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handleEndFromYear = adjustYearSelectionScreen(year => year + 2 - year % 3); + /** * Navigate to back 12 years and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handlePageUpFromYear = adjustYearSelectionScreen(year => year - YEAR_CHUNK); + /** * Navigate forward 12 years and display the year selection screen. * * @param {KeyboardEvent} event the keydown event */ - const handlePageDownFromYear = adjustYearSelectionScreen(year => year + YEAR_CHUNK); + /** * update the focus on a year when the mouse moves. * * @param {MouseEvent} event The mouseover event * @param {HTMLButtonElement} dateEl A year element within the date picker component */ - const handleMouseoverFromYear = yearEl => { if (yearEl.disabled) return; if (yearEl.classList.contains(CALENDAR_YEAR_FOCUSED_CLASS)) return; const focusYear = parseInt(yearEl.dataset.value, 10); const newCalendar = displayYearSelection(yearEl, focusYear); newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus(); -}; // #endregion Calendar Year Event Handling -// #region Focus Handling Event Handling +}; +// #endregion Calendar Year Event Handling + +// #region Focus Handling Event Handling const tabHandler = focusable => { const getFocusableContext = el => { @@ -3881,7 +3626,6 @@ const tabHandler = focusable => { isLastTab }; }; - return { tabAhead(event) { const { @@ -3889,32 +3633,30 @@ const tabHandler = focusable => { isLastTab, isNotFound } = getFocusableContext(event.target); - if (isLastTab || isNotFound) { event.preventDefault(); firstTabStop.focus(); } }, - tabBack(event) { const { lastTabStop, isFirstTab, isNotFound } = getFocusableContext(event.target); - if (isFirstTab || isNotFound) { event.preventDefault(); lastTabStop.focus(); } } - }; }; - const datePickerTabEventHandler = tabHandler(DATE_PICKER_FOCUSABLE); const monthPickerTabEventHandler = tabHandler(MONTH_PICKER_FOCUSABLE); -const yearPickerTabEventHandler = tabHandler(YEAR_PICKER_FOCUSABLE); // #endregion Focus Handling Event Handling +const yearPickerTabEventHandler = tabHandler(YEAR_PICKER_FOCUSABLE); + +// #endregion Focus Handling Event Handling + // #region Date Picker Event Delegation Registration / Component const datePickerEvents = { @@ -3922,63 +3664,49 @@ const datePickerEvents = { [DATE_PICKER_BUTTON]() { toggleCalendar(this); }, - [CALENDAR_DATE]() { selectDate(this); }, - [CALENDAR_MONTH]() { selectMonth(this); }, - [CALENDAR_YEAR]() { selectYear(this); }, - [CALENDAR_PREVIOUS_MONTH]() { displayPreviousMonth(this); }, - [CALENDAR_NEXT_MONTH]() { displayNextMonth(this); }, - [CALENDAR_PREVIOUS_YEAR]() { displayPreviousYear(this); }, - [CALENDAR_NEXT_YEAR]() { displayNextYear(this); }, - [CALENDAR_PREVIOUS_YEAR_CHUNK]() { displayPreviousYearChunk(this); }, - [CALENDAR_NEXT_YEAR_CHUNK]() { displayNextYearChunk(this); }, - [CALENDAR_MONTH_SELECTION]() { const newCalendar = displayMonthSelection(this); newCalendar.querySelector(CALENDAR_MONTH_FOCUSED).focus(); }, - [CALENDAR_YEAR_SELECTION]() { const newCalendar = displayYearSelection(this); newCalendar.querySelector(CALENDAR_YEAR_FOCUSED).focus(); } - }, keyup: { [DATE_PICKER_CALENDAR](event) { const keydown = this.dataset.keydownKeyCode; - if (`${event.keyCode}` !== keydown) { event.preventDefault(); } } - }, keydown: { [DATE_PICKER_EXTERNAL_INPUT](event) { @@ -3986,7 +3714,6 @@ const datePickerEvents = { validateDateInput(this); } }, - [CALENDAR_DATE]: keymap({ Up: handleUpFromDate, ArrowUp: handleUpFromDate, @@ -4044,95 +3771,81 @@ const datePickerEvents = { Tab: yearPickerTabEventHandler.tabAhead, "Shift+Tab": yearPickerTabEventHandler.tabBack }), - [DATE_PICKER_CALENDAR](event) { this.dataset.keydownKeyCode = event.keyCode; }, - [DATE_PICKER](event) { const keyMap = keymap({ Escape: handleEscapeFromCalendar }); keyMap(event); } - }, focusout: { [DATE_PICKER_EXTERNAL_INPUT]() { validateDateInput(this); }, - [DATE_PICKER](event) { if (!this.contains(event.relatedTarget)) { hideCalendar(this); } } - }, input: { [DATE_PICKER_EXTERNAL_INPUT]() { reconcileInputValues(this); updateCalendarIfVisible(this); } - } }; - if (!isIosDevice()) { datePickerEvents.mouseover = { [CALENDAR_DATE_CURRENT_MONTH]() { handleMouseoverFromDate(this); }, - [CALENDAR_MONTH]() { handleMouseoverFromMonth(this); }, - [CALENDAR_YEAR]() { handleMouseoverFromYear(this); } - }; } - const datePicker = behavior(datePickerEvents, { init(root) { selectOrMatches(DATE_PICKER, root).forEach(datePickerEl => { enhanceDatePicker(datePickerEl); }); }, - getDatePickerContext, disable, + ariaDisable, enable, isDateInputInvalid, setCalendarValue, validateDateInput, renderCalendar, updateCalendarIfVisible -}); // #endregion Date Picker Event Delegation Registration / Component +}); + +// #endregion Date Picker Event Delegation Registration / Component module.exports = datePicker; -},{"../../uswds-core/src/js/config":33,"../../uswds-core/src/js/events":34,"../../uswds-core/src/js/utils/active-element":42,"../../uswds-core/src/js/utils/behavior":43,"../../uswds-core/src/js/utils/is-ios-device":46,"../../uswds-core/src/js/utils/sanitizer":47,"../../uswds-core/src/js/utils/select":50,"../../uswds-core/src/js/utils/select-or-matches":49,"receptor/keymap":12}],21:[function(require,module,exports){ +},{"../../uswds-core/src/js/config":36,"../../uswds-core/src/js/events":37,"../../uswds-core/src/js/utils/active-element":45,"../../uswds-core/src/js/utils/behavior":46,"../../uswds-core/src/js/utils/is-ios-device":50,"../../uswds-core/src/js/utils/sanitizer":51,"../../uswds-core/src/js/utils/select":54,"../../uswds-core/src/js/utils/select-or-matches":53,"receptor/keymap":11}],21:[function(require,module,exports){ "use strict"; const behavior = require("../../uswds-core/src/js/utils/behavior"); - const select = require("../../uswds-core/src/js/utils/select"); - const selectOrMatches = require("../../uswds-core/src/js/utils/select-or-matches"); - const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - const { getDatePickerContext, isDateInputInvalid, updateCalendarIfVisible } = require("../../usa-date-picker/src/index"); - const DATE_PICKER_CLASS = `${PREFIX}-date-picker`; const DATE_RANGE_PICKER_CLASS = `${PREFIX}-date-range-picker`; const DATE_RANGE_PICKER_RANGE_START_CLASS = `${DATE_RANGE_PICKER_CLASS}__range-start`; @@ -4142,6 +3855,7 @@ const DATE_RANGE_PICKER = `.${DATE_RANGE_PICKER_CLASS}`; const DATE_RANGE_PICKER_RANGE_START = `.${DATE_RANGE_PICKER_RANGE_START_CLASS}`; const DATE_RANGE_PICKER_RANGE_END = `.${DATE_RANGE_PICKER_RANGE_END_CLASS}`; const DEFAULT_MIN_DATE = "0000-01-01"; + /** * The properties and elements within the date range picker. * @typedef {Object} DateRangePickerContext @@ -4157,14 +3871,11 @@ const DEFAULT_MIN_DATE = "0000-01-01"; * @param {HTMLElement} el the element within the date picker * @returns {DateRangePickerContext} elements */ - const getDateRangePickerContext = el => { const dateRangePickerEl = el.closest(DATE_RANGE_PICKER); - if (!dateRangePickerEl) { throw new Error(`Element is missing outer ${DATE_RANGE_PICKER}`); } - const rangeStartEl = dateRangePickerEl.querySelector(DATE_RANGE_PICKER_RANGE_START); const rangeEndEl = dateRangePickerEl.querySelector(DATE_RANGE_PICKER_RANGE_END); return { @@ -4173,13 +3884,12 @@ const getDateRangePickerContext = el => { rangeEndEl }; }; + /** * handle update from range start date picker * * @param {HTMLElement} el an element within the date range picker */ - - const handleRangeStartUpdate = el => { const { dateRangePickerEl, @@ -4190,7 +3900,6 @@ const handleRangeStartUpdate = el => { internalInputEl } = getDatePickerContext(rangeStartEl); const updatedDate = internalInputEl.value; - if (updatedDate && !isDateInputInvalid(internalInputEl)) { rangeEndEl.dataset.minDate = updatedDate; rangeEndEl.dataset.rangeDate = updatedDate; @@ -4200,16 +3909,14 @@ const handleRangeStartUpdate = el => { rangeEndEl.dataset.rangeDate = ""; rangeEndEl.dataset.defaultDate = ""; } - updateCalendarIfVisible(rangeEndEl); }; + /** * handle update from range start date picker * * @param {HTMLElement} el an element within the date range picker */ - - const handleRangeEndUpdate = el => { const { dateRangePickerEl, @@ -4220,7 +3927,6 @@ const handleRangeEndUpdate = el => { internalInputEl } = getDatePickerContext(rangeEndEl); const updatedDate = internalInputEl.value; - if (updatedDate && !isDateInputInvalid(internalInputEl)) { rangeStartEl.dataset.maxDate = updatedDate; rangeStartEl.dataset.rangeDate = updatedDate; @@ -4230,35 +3936,28 @@ const handleRangeEndUpdate = el => { rangeStartEl.dataset.rangeDate = ""; rangeStartEl.dataset.defaultDate = ""; } - updateCalendarIfVisible(rangeStartEl); }; + /** * Enhance an input with the date picker elements * * @param {HTMLElement} el The initial wrapping element of the date range picker component */ - - const enhanceDateRangePicker = el => { const dateRangePickerEl = el.closest(DATE_RANGE_PICKER); const [rangeStart, rangeEnd] = select(DATE_PICKER, dateRangePickerEl); - if (!rangeStart) { throw new Error(`${DATE_RANGE_PICKER} is missing inner two '${DATE_PICKER}' elements`); } - if (!rangeEnd) { throw new Error(`${DATE_RANGE_PICKER} is missing second '${DATE_PICKER}' element`); } - rangeStart.classList.add(DATE_RANGE_PICKER_RANGE_START_CLASS); rangeEnd.classList.add(DATE_RANGE_PICKER_RANGE_END_CLASS); - if (!dateRangePickerEl.dataset.minDate) { dateRangePickerEl.dataset.minDate = DEFAULT_MIN_DATE; } - const { minDate } = dateRangePickerEl.dataset; @@ -4267,26 +3966,21 @@ const enhanceDateRangePicker = el => { const { maxDate } = dateRangePickerEl.dataset; - if (maxDate) { rangeStart.dataset.maxDate = maxDate; rangeEnd.dataset.maxDate = maxDate; } - handleRangeStartUpdate(dateRangePickerEl); handleRangeEndUpdate(dateRangePickerEl); }; - const dateRangePicker = behavior({ "input change": { [DATE_RANGE_PICKER_RANGE_START]() { handleRangeStartUpdate(this); }, - [DATE_RANGE_PICKER_RANGE_END]() { handleRangeEndUpdate(this); } - } }, { init(root) { @@ -4294,23 +3988,18 @@ const dateRangePicker = behavior({ enhanceDateRangePicker(dateRangePickerEl); }); } - }); module.exports = dateRangePicker; -},{"../../usa-date-picker/src/index":20,"../../uswds-core/src/js/config":33,"../../uswds-core/src/js/utils/behavior":43,"../../uswds-core/src/js/utils/select":50,"../../uswds-core/src/js/utils/select-or-matches":49}],22:[function(require,module,exports){ +},{"../../usa-date-picker/src/index":20,"../../uswds-core/src/js/config":36,"../../uswds-core/src/js/utils/behavior":46,"../../uswds-core/src/js/utils/select":54,"../../uswds-core/src/js/utils/select-or-matches":53}],22:[function(require,module,exports){ "use strict"; const selectOrMatches = require("../../uswds-core/src/js/utils/select-or-matches"); - const behavior = require("../../uswds-core/src/js/utils/behavior"); - const Sanitizer = require("../../uswds-core/src/js/utils/sanitizer"); - const { prefix: PREFIX } = require("../../uswds-core/src/js/config"); - const DROPZONE_CLASS = `${PREFIX}-file-input`; const DROPZONE = `.${DROPZONE_CLASS}`; const INPUT_CLASS = `${PREFIX}-file-input__input`; @@ -4326,7 +4015,6 @@ const ACCEPTED_FILE_MESSAGE_CLASS = `${PREFIX}-file-input__accepted-files-messag const DRAG_TEXT_CLASS = `${PREFIX}-file-input__drag-text`; const DRAG_CLASS = `${PREFIX}-file-input--drag`; const LOADING_CLASS = "is-loading"; -const HIDDEN_CLASS = "display-none"; const INVALID_FILE_CLASS = "has-invalid-file"; const GENERIC_PREVIEW_CLASS_NAME = `${PREFIX}-file-input__preview-image`; const GENERIC_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--generic`; @@ -4334,8 +4022,11 @@ const PDF_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--pdf`; const WORD_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--word`; const VIDEO_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--video`; const EXCEL_PREVIEW_CLASS = `${GENERIC_PREVIEW_CLASS_NAME}--excel`; +const SR_ONLY_CLASS = `${PREFIX}-sr-only`; const SPACER_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; let TYPE_IS_VALID = Boolean(true); // logic gate for change listener +let DEFAULT_ARIA_LABEL_TEXT = ""; +let DEFAULT_FILE_STATUS_TEXT = ""; /** * The properties and elements within the file input. @@ -4351,27 +4042,23 @@ let TYPE_IS_VALID = Boolean(true); // logic gate for change listener * @param {HTMLElement} el the element within the file input * @returns {FileInputContext} elements */ - const getFileInputContext = el => { const dropZoneEl = el.closest(DROPZONE); - if (!dropZoneEl) { throw new Error(`Element is missing outer ${DROPZONE}`); } - const inputEl = dropZoneEl.querySelector(INPUT); return { dropZoneEl, inputEl }; }; + /** * Disable the file input component * * @param {HTMLElement} el An element within the file input component */ - - const disable = el => { const { dropZoneEl, @@ -4379,15 +4066,25 @@ const disable = el => { } = getFileInputContext(el); inputEl.disabled = true; dropZoneEl.classList.add(DISABLED_CLASS); - dropZoneEl.setAttribute("aria-disabled", "true"); }; + /** - * Enable the file input component + * Set aria-disabled attribute to file input component * * @param {HTMLElement} el An element within the file input component */ +const ariaDisable = el => { + const { + dropZoneEl + } = getFileInputContext(el); + dropZoneEl.classList.add(DISABLED_CLASS); +}; - +/** + * Enable the file input component + * + * @param {HTMLElement} el An element within the file input component + */ const enable = el => { const { dropZoneEl, @@ -4397,177 +4094,284 @@ const enable = el => { dropZoneEl.classList.remove(DISABLED_CLASS); dropZoneEl.removeAttribute("aria-disabled"); }; + /** * * @param {String} s special characters * @returns {String} replaces specified values */ - - const replaceName = s => { const c = s.charCodeAt(0); if (c === 32) return "-"; if (c >= 65 && c <= 90) return `img_${s.toLowerCase()}`; return `__${("000", c.toString(16)).slice(-4)}`; }; + /** * Creates an ID name for each file that strips all invalid characters. * @param {String} name - name of the file added to file input (searchvalue) * @returns {String} same characters as the name with invalid chars removed (newvalue) */ +const makeSafeForID = name => name.replace(/[^a-z0-9]/g, replaceName); - -const makeSafeForID = name => name.replace(/[^a-z0-9]/g, replaceName); // Takes a generated safe ID and creates a unique ID. - - +// Takes a generated safe ID and creates a unique ID. const createUniqueID = name => `${name}-${Math.floor(Date.now().toString() / 1000)}`; + /** - * Builds full file input component - * @param {HTMLElement} fileInputEl - original file input on page - * @returns {HTMLElement|HTMLElement} - Instructions, target area div + * Determines if the singular or plural item label should be used + * Determination is based on the presence of the `multiple` attribute + * + * @param {HTMLInputElement} fileInputEl - The input element. + * @returns {HTMLDivElement} The singular or plural version of "item" */ - - -const buildFileInput = fileInputEl => { +const getItemsLabel = fileInputEl => { const acceptsMultiple = fileInputEl.hasAttribute("multiple"); + const itemsLabel = acceptsMultiple ? "files" : "file"; + return itemsLabel; +}; + +/** + * Scaffold the file input component with a parent wrapper and + * Create a target area overlay for drag and drop functionality + * + * @param {HTMLInputElement} fileInputEl - The input element. + * @returns {HTMLDivElement} The drag and drop target area. + */ +const createTargetArea = fileInputEl => { const fileInputParent = document.createElement("div"); const dropTarget = document.createElement("div"); const box = document.createElement("div"); - const instructions = document.createElement("div"); - const disabled = fileInputEl.hasAttribute("disabled"); - let defaultAriaLabel; // Adds class names and other attributes + // Adds class names and other attributes fileInputEl.classList.remove(DROPZONE_CLASS); fileInputEl.classList.add(INPUT_CLASS); fileInputParent.classList.add(DROPZONE_CLASS); box.classList.add(BOX_CLASS); - instructions.classList.add(INSTRUCTIONS_CLASS); - instructions.setAttribute("aria-hidden", "true"); - dropTarget.classList.add(TARGET_CLASS); // Encourage screenreader to read out aria changes immediately following upload status change - - fileInputEl.setAttribute("aria-live", "polite"); // Adds child elements to the DOM + dropTarget.classList.add(TARGET_CLASS); + // Adds child elements to the DOM + dropTarget.prepend(box); fileInputEl.parentNode.insertBefore(dropTarget, fileInputEl); fileInputEl.parentNode.insertBefore(fileInputParent, dropTarget); dropTarget.appendChild(fileInputEl); fileInputParent.appendChild(dropTarget); - fileInputEl.parentNode.insertBefore(instructions, fileInputEl); - fileInputEl.parentNode.insertBefore(box, fileInputEl); // Disabled styling + return dropTarget; +}; + +/** + * Build the visible element with default interaction instructions. + * + * @param {HTMLInputElement} fileInputEl - The input element. + * @returns {HTMLDivElement} The container for visible interaction instructions. + */ +const createVisibleInstructions = fileInputEl => { + const fileInputParent = fileInputEl.closest(DROPZONE); + const itemsLabel = getItemsLabel(fileInputEl); + const instructions = document.createElement("div"); + const dragText = `Drag ${itemsLabel} here or`; + const chooseText = "choose from folder"; - if (disabled) { - disable(fileInputEl); - } // Sets instruction test and aria-label based on whether or not multiple files are accepted + // Create instructions text for aria-label + DEFAULT_ARIA_LABEL_TEXT = `${dragText} ${chooseText}`; + // Adds class names and other attributes + instructions.classList.add(INSTRUCTIONS_CLASS); + instructions.setAttribute("aria-hidden", "true"); - if (acceptsMultiple) { - defaultAriaLabel = "No files selected"; - instructions.innerHTML = Sanitizer.escapeHTML`Drag files here or choose from folder`; - fileInputEl.setAttribute("aria-label", defaultAriaLabel); - fileInputEl.setAttribute("data-default-aria-label", defaultAriaLabel); - } else { - defaultAriaLabel = "No file selected"; - instructions.innerHTML = Sanitizer.escapeHTML`Drag file here or choose from folder`; - fileInputEl.setAttribute("aria-label", defaultAriaLabel); - fileInputEl.setAttribute("data-default-aria-label", defaultAriaLabel); - } // IE11 and Edge do not support drop files on file inputs, so we've removed text that indicates that + // Add initial instructions for input usage + fileInputEl.setAttribute("aria-label", DEFAULT_ARIA_LABEL_TEXT); + instructions.innerHTML = Sanitizer.escapeHTML`${dragText} ${chooseText}`; + // Add the instructions element to the DOM + fileInputEl.parentNode.insertBefore(instructions, fileInputEl); + // IE11 and Edge do not support drop files on file inputs, so we've removed text that indicates that if (/rv:11.0/i.test(navigator.userAgent) || /Edge\/\d./i.test(navigator.userAgent)) { fileInputParent.querySelector(`.${DRAG_TEXT_CLASS}`).outerHTML = ""; } + return instructions; +}; + +/** + * Build a screen reader-only message element that contains file status updates and + * Create and set the default file status message + * + * @param {HTMLInputElement} fileInputEl - The input element. + */ +const createSROnlyStatus = fileInputEl => { + const statusEl = document.createElement("div"); + const itemsLabel = getItemsLabel(fileInputEl); + const fileInputParent = fileInputEl.closest(DROPZONE); + const fileInputTarget = fileInputEl.closest(`.${TARGET_CLASS}`); + DEFAULT_FILE_STATUS_TEXT = `No ${itemsLabel} selected.`; + + // Adds class names and other attributes + statusEl.classList.add(SR_ONLY_CLASS); + statusEl.setAttribute("aria-live", "polite"); + + // Add initial file status message + statusEl.textContent = DEFAULT_FILE_STATUS_TEXT; + // Add the status element to the DOM + fileInputParent.insertBefore(statusEl, fileInputTarget); +}; + +/** + * Scaffold the component with all required elements + * + * @param {HTMLInputElement} fileInputEl - The original input element. + */ +const enhanceFileInput = fileInputEl => { + const isInputDisabled = fileInputEl.hasAttribute("aria-disabled") || fileInputEl.hasAttribute("disabled"); + const dropTarget = createTargetArea(fileInputEl); + const instructions = createVisibleInstructions(fileInputEl); + const { + dropZoneEl + } = getFileInputContext(fileInputEl); + if (isInputDisabled) { + dropZoneEl.classList.add(DISABLED_CLASS); + } else { + createSROnlyStatus(fileInputEl); + } return { instructions, dropTarget }; }; + /** - * Removes image previews, we want to start with a clean list every time files are added to the file input - * @param {HTMLElement} dropTarget - target area div that encases the input - * @param {HTMLElement} instructions - text to inform users to drag or select files + * Removes image previews + * We want to start with a clean list every time files are added to the file input + * + * @param {HTMLDivElement} dropTarget - The drag and drop target area. + * @param {HTMLDivElement} instructions - The container for visible interaction instructions. */ - - -const removeOldPreviews = (dropTarget, instructions, inputAriaLabel) => { +const removeOldPreviews = (dropTarget, instructions) => { const filePreviews = dropTarget.querySelectorAll(`.${PREVIEW_CLASS}`); - const fileInputElement = dropTarget.querySelector(INPUT); const currentPreviewHeading = dropTarget.querySelector(`.${PREVIEW_HEADING_CLASS}`); const currentErrorMessage = dropTarget.querySelector(`.${ACCEPTED_FILE_MESSAGE_CLASS}`); + /** * finds the parent of the passed node and removes the child * @param {HTMLElement} node */ - const removeImages = node => { node.parentNode.removeChild(node); - }; // Remove the heading above the previews - + }; + // Remove the heading above the previews if (currentPreviewHeading) { currentPreviewHeading.outerHTML = ""; - } // Remove existing error messages - + } + // Remove existing error messages if (currentErrorMessage) { currentErrorMessage.outerHTML = ""; dropTarget.classList.remove(INVALID_FILE_CLASS); - } // Get rid of existing previews if they exist, show instructions - + } + // Get rid of existing previews if they exist, show instructions if (filePreviews !== null) { if (instructions) { - instructions.classList.remove(HIDDEN_CLASS); + instructions.removeAttribute("hidden"); } - - fileInputElement.setAttribute("aria-label", inputAriaLabel); Array.prototype.forEach.call(filePreviews, removeImages); } }; + +/** + * Update the screen reader-only status message after interaction + * + * @param {HTMLDivElement} statusElement - The screen reader-only container for file status updates. + * @param {Object} fileNames - The selected files found in the fileList object. + * @param {Array} fileStore - The array of uploaded file names created from the fileNames object. + */ +const updateStatusMessage = (statusElement, fileNames, fileStore) => { + const statusEl = statusElement; + let statusMessage = DEFAULT_FILE_STATUS_TEXT; + + // If files added, update the status message with file name(s) + if (fileNames.length === 1) { + statusMessage = `You have selected the file: ${fileStore}`; + } else if (fileNames.length > 1) { + statusMessage = `You have selected ${fileNames.length} files: ${fileStore.join(", ")}`; + } + + // Add delay to encourage screen reader readout + setTimeout(() => { + statusEl.textContent = statusMessage; + }, 1000); +}; + +/** + * Show the preview heading, hide the initial instructions and + * Update the aria-label with new instructions text + * + * @param {HTMLInputElement} fileInputEl - The input element. + * @param {Object} fileNames - The selected files found in the fileList object. + */ +const addPreviewHeading = (fileInputEl, fileNames) => { + const filePreviewsHeading = document.createElement("div"); + const dropTarget = fileInputEl.closest(`.${TARGET_CLASS}`); + const instructions = dropTarget.querySelector(`.${INSTRUCTIONS_CLASS}`); + let changeItemText = "Change file"; + let previewHeadingText = ""; + if (fileNames.length === 1) { + previewHeadingText = Sanitizer.escapeHTML`Selected file ${changeItemText}`; + } else if (fileNames.length > 1) { + changeItemText = "Change files"; + previewHeadingText = Sanitizer.escapeHTML`${fileNames.length} files selected ${changeItemText}`; + } + + // Hides null state content and sets preview heading + instructions.setAttribute("hidden", "true"); + filePreviewsHeading.classList.add(PREVIEW_HEADING_CLASS); + filePreviewsHeading.innerHTML = previewHeadingText; + dropTarget.insertBefore(filePreviewsHeading, instructions); + + // Update aria label to match the visible action text + fileInputEl.setAttribute("aria-label", changeItemText); +}; + /** * When new files are applied to file input, this function generates previews * and removes old ones. + * * @param {event} e - * @param {HTMLElement} fileInputEl - file input element - * @param {HTMLElement} instructions - text to inform users to drag or select files - * @param {HTMLElement} dropTarget - target area div that encases the input + * @param {HTMLInputElement} fileInputEl - The input element. + * @param {HTMLDivElement} instructions - The container for visible interaction instructions. + * @param {HTMLDivElement} dropTarget - The drag and drop target area. */ - const handleChange = (e, fileInputEl, instructions, dropTarget) => { const fileNames = e.target.files; - const filePreviewsHeading = document.createElement("div"); - const inputAriaLabel = fileInputEl.dataset.defaultAriaLabel; - const fileStore = []; // First, get rid of existing previews + const inputParent = dropTarget.closest(`.${DROPZONE_CLASS}`); + const statusElement = inputParent.querySelector(`.${SR_ONLY_CLASS}`); + const fileStore = []; - removeOldPreviews(dropTarget, instructions, inputAriaLabel); // Then, iterate through files list and: - // 1. Add selected file list names to aria-label - // 2. Create previews + // First, get rid of existing previews + removeOldPreviews(dropTarget, instructions); + // Then, iterate through files list and create previews for (let i = 0; i < fileNames.length; i += 1) { const reader = new FileReader(); - const fileName = fileNames[i].name; // Push updated file names into the store array - - fileStore.push(fileName); // read out the store array via aria-label, wording options vary based on file count - - if (i === 0) { - fileInputEl.setAttribute("aria-label", `You have selected the file: ${fileName}`); - } else if (i >= 1) { - fileInputEl.setAttribute("aria-label", `You have selected ${fileNames.length} files: ${fileStore.join(", ")}`); - } // Starts with a loading image while preview is created + const fileName = fileNames[i].name; + let imageId; + // Push updated file names into the store array + fileStore.push(fileName); + // Starts with a loading image while preview is created reader.onloadstart = function createLoadingImage() { - const imageId = createUniqueID(makeSafeForID(fileName)); + imageId = createUniqueID(makeSafeForID(fileName)); instructions.insertAdjacentHTML("afterend", Sanitizer.escapeHTML`