@uswds/uswds
Version:
Open source UI components and visual style guide for U.S. government websites
1,606 lines (1,514 loc) • 808 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[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);
},{}],2:[function(require,module,exports){
"use strict";
/* global define, KeyboardEvent, module */
(function () {
var keyboardeventKeyPolyfill = {
polyfill: polyfill,
keys: {
3: 'Cancel',
6: 'Help',
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
16: 'Shift',
17: 'Control',
18: 'Alt',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
28: 'Convert',
29: 'NonConvert',
30: 'Accept',
31: 'ModeChange',
32: ' ',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
41: 'Select',
42: 'Print',
43: 'Execute',
44: 'PrintScreen',
45: 'Insert',
46: 'Delete',
48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],
91: 'OS',
93: 'ContextMenu',
144: 'NumLock',
145: 'ScrollLock',
181: 'VolumeMute',
182: 'VolumeDown',
183: 'VolumeUp',
186: [';', ':'],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],
219: ['[', '{'],
220: ['\\', '|'],
221: [']', '}'],
222: ["'", '"'],
224: 'Meta',
225: 'AltGraph',
246: 'Attn',
247: 'CrSel',
248: 'ExSel',
249: 'EraseEof',
250: 'Play',
251: 'ZoomOut'
}
};
// Function keys (F1-24).
var i;
for (i = 1; i < 25; i++) {
keyboardeventKeyPolyfill.keys[111 + i] = 'F' + i;
}
// 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`.
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') {
module.exports = keyboardeventKeyPolyfill;
} else if (window) {
window.keyboardeventKeyPolyfill = keyboardeventKeyPolyfill;
}
})();
},{}],3:[function(require,module,exports){
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
'use strict';
/* 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
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
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
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]];
}
}
}
}
return to;
};
},{}],4:[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({
type: _type
}, listener);
});
} else {
listener.type = type;
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]);
return memo.concat(listeners);
}, []);
return assign({
add: function addBehavior(element) {
listeners.forEach(function (listener) {
element.addEventListener(listener.type, listener.delegate, listener.options);
});
},
remove: function removeBehavior(element) {
listeners.forEach(function (listener) {
element.removeEventListener(listener.type, listener.delegate, listener.options);
});
}
}, props);
};
},{"../delegate":6,"../delegateAll":7,"object-assign":3}],5:[function(require,module,exports){
"use strict";
module.exports = function compose(functions) {
return function (e) {
return functions.some(function (fn) {
return fn.call(this, e) === false;
}, this);
};
};
},{}],6:[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":1}],7:[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
// 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;
}, []);
return compose(delegates);
};
},{"../compose":5,"../delegate":6}],8:[function(require,module,exports){
"use strict";
module.exports = function ignore(element, fn) {
return function ignorance(e) {
if (element !== e.target && !element.contains(e.target)) {
return fn.call(this, e);
}
};
};
},{}],9:[function(require,module,exports){
"use strict";
module.exports = {
behavior: require('./behavior'),
delegate: require('./delegate'),
delegateAll: require('./delegateAll'),
ignore: require('./ignore'),
keymap: require('./keymap')
};
},{"./behavior":4,"./delegate":6,"./delegateAll":7,"./ignore":8,"./keymap":10}],10:[function(require,module,exports){
"use strict";
require('keyboardevent-key-polyfill');
// these are the only relevant modifiers supported on all platforms,
// according to MDN:
// <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState>
const MODIFIERS = {
'Alt': 'altKey',
'Control': 'ctrlKey',
'Ctrl': 'ctrlKey',
'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) {
key = [modifier, key].join(MODIFIER_SEPARATOR);
}
}
}
return key;
};
module.exports = function keymap(keys) {
const hasModifiers = Object.keys(keys).some(function (key) {
return key.indexOf(MODIFIER_SEPARATOR) > -1;
});
return function (event) {
var key = getEventKey(event, hasModifiers);
return [key, key.toLowerCase()].reduce(function (result, _key) {
if (_key in keys) {
result = keys[key].call(this, event);
}
return result;
}, undefined);
};
};
module.exports.MODIFIERS = MODIFIERS;
},{"keyboardevent-key-polyfill":2}],11:[function(require,module,exports){
"use strict";
module.exports = function once(listener, options) {
var wrapped = function wrappedOnce(e) {
e.currentTarget.removeEventListener(e.type, wrapped, options);
return listener.call(this, e);
};
return wrapped;
};
},{}],12:[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":34,"../../uswds-core/src/js/events":35,"../../uswds-core/src/js/utils/behavior":39,"../../uswds-core/src/js/utils/toggle-form-input":49}],13:[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 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<HTMLButtonElement>}
*/
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.
*
* @param {HTMLButtonElement} button
* @param {boolean?} expanded If no state is provided, the current
* 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);
// XXX multiselectable is opt-in, to preserve legacy behavior
const multiselectable = accordion.hasAttribute(MULTISELECTABLE);
if (safeExpanded && !multiselectable) {
getAccordionButtons(accordion).forEach(other => {
if (other !== button) {
toggle(other, false);
}
});
}
};
/**
* @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]() {
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
// that we are still visible, so the user isn't confused.
if (!isElementInViewport(this)) this.scrollIntoView();
}
}
}
}, {
init(root) {
select(BUTTON, root).forEach(button => {
const expanded = button.getAttribute(EXPANDED) === "true";
toggleButton(button, expanded);
});
},
ACCORDION,
BUTTON,
show: showButton,
hide: hideButton,
toggle: toggleButton,
getButtons: getAccordionButtons
});
module.exports = accordion;
},{"../../uswds-core/src/js/config":34,"../../uswds-core/src/js/events":35,"../../uswds-core/src/js/utils/behavior":39,"../../uswds-core/src/js/utils/is-in-viewport":42,"../../uswds-core/src/js/utils/select":47,"../../uswds-core/src/js/utils/toggle":50}],14:[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]: {
[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":34,"../../uswds-core/src/js/events":35,"../../uswds-core/src/js/utils/behavior":39,"../../uswds-core/src/js/utils/select":47,"../../uswds-core/src/js/utils/toggle":50}],15:[function(require,module,exports){
"use strict";
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":39,"receptor/keymap":10}],16:[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_CLASS = `${PREFIX}-character-count`;
const CHARACTER_COUNT = `.${CHARACTER_COUNT_CLASS}`;
const FORM_GROUP_CLASS = `${PREFIX}-form-group`;
const FORM_GROUP_ERROR_CLASS = `${FORM_GROUP_CLASS}--error`;
const FORM_GROUP = `.${FORM_GROUP_CLASS}`;
const LABEL_CLASS = `${PREFIX}-label`;
const LABEL_ERROR_CLASS = `${LABEL_CLASS}--error`;
const INPUT = `.${PREFIX}-character-count__field`;
const INPUT_ERROR_CLASS = `${PREFIX}-input--error`;
const MESSAGE = `.${PREFIX}-character-count__message`;
const VALIDATION_MESSAGE = "The content is too long.";
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, form group, label, and message elements for an character count input
*
* @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element
* @returns {CharacterCountElements} elements The root form group, input ID, label, and message element.
*/
const getCharacterCountElements = inputEl => {
const characterCountEl = inputEl.closest(CHARACTER_COUNT);
if (!characterCountEl) {
throw new Error(`${INPUT} is missing outer ${CHARACTER_COUNT}`);
}
const formGroupEl = characterCountEl.querySelector(FORM_GROUP);
const inputID = inputEl.getAttribute("id");
const labelEl = document.querySelector(`label[for=${inputID}]`);
const messageEl = characterCountEl.querySelector(MESSAGE);
if (!messageEl) {
throw new Error(`${CHARACTER_COUNT} is missing inner ${MESSAGE}`);
}
return {
characterCountEl,
formGroupEl,
inputID,
labelEl,
messageEl
};
};
/**
* Move maxlength attribute to a data attribute on usa-character-count
*
* @param {HTMLInputElement|HTMLTextAreaElement} inputEl The character count input element
*/
const setDataLength = inputEl => {
const {
characterCountEl
} = getCharacterCountElements(inputEl);
const maxlength = inputEl.getAttribute("maxlength");
if (!maxlength) return;
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} ${DEFAULT_STATUS_LABEL}`;
} else {
const difference = Math.abs(maxLength - currentLength);
const characters = `character${difference === 1 ? "" : "s"}`;
const guidance = currentLength > maxLength ? "over limit" : "left";
newMessage = `${difference} ${characters} ${guidance}`;
}
return newMessage;
};
/**
* Updates the character count status for screen readers after a 1000ms delay.
*
* @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);
/**
* 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,
labelEl,
formGroupEl
} = getCharacterCountElements(inputEl);
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("");
}
if (formGroupEl) {
formGroupEl.classList.toggle(FORM_GROUP_ERROR_CLASS, isOverLimit);
}
if (labelEl) {
labelEl.classList.toggle(LABEL_ERROR_CLASS, isOverLimit);
}
inputEl.classList.toggle(INPUT_ERROR_CLASS, isOverLimit);
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 => enhanceCharacterCount(input));
},
FORM_GROUP_ERROR_CLASS,
LABEL_ERROR_CLASS,
INPUT_ERROR_CLASS,
MESSAGE_INVALID_CLASS,
VALIDATION_MESSAGE,
STATUS_MESSAGE_CLASS,
STATUS_MESSAGE_SR_ONLY_CLASS,
DEFAULT_STATUS_LABEL,
createStatusMessages,
getCountMessage,
updateCountMessage
});
module.exports = characterCount;
},{"../../uswds-core/src/js/config":34,"../../uswds-core/src/js/utils/behavior":39,"../../uswds-core/src/js/utils/debounce":40,"../../uswds-core/src/js/utils/select":47}],17:[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`;
const INPUT_CLASS = `${COMBO_BOX_CLASS}__input`;
const CLEAR_INPUT_BUTTON_CLASS = `${COMBO_BOX_CLASS}__clear-input`;
const CLEAR_INPUT_BUTTON_WRAPPER_CLASS = `${CLEAR_INPUT_BUTTON_CLASS}__wrapper`;
const INPUT_BUTTON_SEPARATOR_CLASS = `${COMBO_BOX_CLASS}__input-button-separator`;
const TOGGLE_LIST_BUTTON_CLASS = `${COMBO_BOX_CLASS}__toggle-list`;
const TOGGLE_LIST_BUTTON_WRAPPER_CLASS = `${TOGGLE_LIST_BUTTON_CLASS}__wrapper`;
const LIST_CLASS = `${COMBO_BOX_CLASS}__list`;
const LIST_OPTION_CLASS = `${COMBO_BOX_CLASS}__list-option`;
const LIST_OPTION_FOCUSED_CLASS = `${LIST_OPTION_CLASS}--focused`;
const LIST_OPTION_SELECTED_CLASS = `${LIST_OPTION_CLASS}--selected`;
const STATUS_CLASS = `${COMBO_BOX_CLASS}__status`;
const COMBO_BOX = `.${COMBO_BOX_CLASS}`;
const SELECT = `.${SELECT_CLASS}`;
const INPUT = `.${INPUT_CLASS}`;
const CLEAR_INPUT_BUTTON = `.${CLEAR_INPUT_BUTTON_CLASS}`;
const TOGGLE_LIST_BUTTON = `.${TOGGLE_LIST_BUTTON_CLASS}`;
const LIST = `.${LIST_CLASS}`;
const LIST_OPTION = `.${LIST_OPTION_CLASS}`;
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 = (el, value = "") => {
const elementToChange = el;
elementToChange.value = value;
const event = new CustomEvent("change", {
bubbles: true,
cancelable: true,
detail: {
value
}
});
elementToChange.dispatchEvent(event);
};
/**
* The elements within the combo box.
* @typedef {Object} ComboBoxContext
* @property {HTMLElement} comboBoxEl
* @property {HTMLSelectElement} selectEl
* @property {HTMLInputElement} inputEl
* @property {HTMLUListElement} listEl
* @property {HTMLDivElement} statusEl
* @property {HTMLLIElement} focusedOptionEl
* @property {HTMLLIElement} selectedOptionEl
* @property {HTMLButtonElement} toggleListBtnEl
* @property {HTMLButtonElement} clearInputBtnEl
* @property {boolean} isPristine
* @property {boolean} disableFiltering
*/
/**
* Get an object of elements belonging directly to the given
* combo box component.
*
* @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);
const statusEl = comboBoxEl.querySelector(STATUS);
const focusedOptionEl = comboBoxEl.querySelector(LIST_OPTION_FOCUSED);
const selectedOptionEl = comboBoxEl.querySelector(LIST_OPTION_SELECTED);
const toggleListBtnEl = comboBoxEl.querySelector(TOGGLE_LIST_BUTTON);
const clearInputBtnEl = comboBoxEl.querySelector(CLEAR_INPUT_BUTTON);
const isPristine = comboBoxEl.classList.contains(COMBO_BOX_PRISTINE_CLASS);
const disableFiltering = comboBoxEl.dataset.disableFiltering === "true";
return {
comboBoxEl,
selectEl,
inputEl,
listEl,
statusEl,
focusedOptionEl,
selectedOptionEl,
toggleListBtnEl,
clearInputBtnEl,
isPristine,
disableFiltering
};
};
/**
* Disable the combo-box component
*
* @param {HTMLInputElement} el An element within the combo box component
*/
const disable = el => {
const {
inputEl,
toggleListBtnEl,
clearInputBtnEl
} = getComboBoxContext(el);
clearInputBtnEl.hidden = true;
clearInputBtnEl.disabled = true;
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,
toggleListBtnEl,
clearInputBtnEl
} = getComboBoxContext(el);
clearInputBtnEl.hidden = false;
clearInputBtnEl.disabled = false;
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`;
const listIdLabel = `${selectId}-label`;
const additionalAttributes = [];
const {
defaultValue
} = comboBoxEl.dataset;
const {
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 <ul> 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");
selectEl.classList.add("usa-sr-only", SELECT_CLASS);
selectEl.id = "";
selectEl.value = "";
["required", "aria-label", "aria-labelledby"].forEach(name => {
if (selectEl.hasAttribute(name)) {
const value = selectEl.getAttribute(name);
additionalAttributes.push({
[name]: value
});
selectEl.removeAttribute(name);
}
});
// sanitize doesn't like functions in template literals
const input = document.createElement("input");
input.setAttribute("id", selectId);
input.setAttribute("aria-owns", listId);
input.setAttribute("aria-controls", listId);
input.setAttribute("aria-autocomplete", "list");
input.setAttribute("aria-expanded", "false");
input.setAttribute("autocapitalize", "off");
input.setAttribute("autocomplete", "off");
input.setAttribute("class", INPUT_CLASS);
input.setAttribute("type", "text");
input.setAttribute("role", "combobox");
additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => {
const value = Sanitizer.escapeHTML`${attr[key]}`;
input.setAttribute(key, value);
}));
comboBoxEl.insertAdjacentElement("beforeend", input);
comboBoxEl.insertAdjacentHTML("beforeend", Sanitizer.escapeHTML`
<span class="${CLEAR_INPUT_BUTTON_WRAPPER_CLASS}" tabindex="-1">
<button type="button" class="${CLEAR_INPUT_BUTTON_CLASS}" aria-label="Clear the select contents"> </button>
</span>
<span class="${INPUT_BUTTON_SEPARATOR_CLASS}"> </span>
<span class="${TOGGLE_LIST_BUTTON_WRAPPER_CLASS}" tabindex="-1">
<button type="button" tabindex="-1" class="${TOGGLE_LIST_BUTTON_CLASS}" aria-label="Toggle the dropdown list"> </button>
</span>
<ul
tabindex="-1"
id="${listId}"
class="${LIST_CLASS}"
role="listbox"
aria-labelledby="${listIdLabel}"
hidden>
</ul>
<div class="${STATUS_CLASS} usa-sr-only" role="status"></div>`);
if (selectedOption) {
const {
inputEl
} = getComboBoxContext(comboBoxEl);
changeElementValue(selectEl, selectedOption.value);
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.
*
* @param {HTMLElement} el An anchor element within the combo box component
* @param {HTMLElement} nextEl An element within the combo box component
* @param {Object} options options
* @param {boolean} options.skipFocus skip focus of highlighted item
* @param {boolean} options.preventScroll should skip procedure to scroll to element
*/
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
});
}
} else {
inputEl.setAttribute("aria-activedescendant", "");
inputEl.focus();
}
};
/**
* Generate a dynamic regular expression based off of a replaceable and possibly filtered value.
*
* @param {string} el An element within the combo box component
* @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 = (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,
selectEl,
inputEl,
listEl,
statusEl,
isPristine,
disableFiltering
} = getComboBoxContext(el);
let selectedItemId;
let firstFoundId;
const listOptionBaseId = `${listEl.id}--option-`;
const inputValue = (inputEl.value || "").toLowerCase();
const filter = comboBoxEl.dataset.filter || DEFAULT_FILTER;
const regex = generateDynamicRegExp(filter, inputValue, comboBoxEl.dataset);
let options = [];
const optionsStartsWith = [];
const optionsContains = [];
const optionList = [...selectEl.options];
/**
* Builds and sorts options array.
*
* Option param is passed through regex test before passing into this function.
* When filtering is enabled, the array will be sorted by options that start with the query, followed by
* options that contain the query.
* When filtering is disabled, all options will be included in the array unsorted.
*
* These array items will populate the list that is displayed to the user after a search query is entered.
* Array attributes are also used to set option IDs and aria-setsize attributes.
*
* @param {HTMLOptionElement} option - Option element from select array
*/
const buildOptionsArray = option => {
if (disableFiltering || isPristine) {
options.push(option);
return;
}
const matchStartsWith = option.text.toLowerCase().startsWith(inputValue);
if (matchStartsWith) {
optionsStartsWith.push(option);
} else {
optionsContains.push(option);
}
options = [...optionsStartsWith, ...optionsContains];
};
/**
* Compares option text to query using generated regex filter.
*
* @param {HTMLOptionElement} option
* @returns {boolean} - True when option text matches user input query.
*/
const optionMatchesQuery = option => regex.test(option.text);
/**
* Logic check to determine if options array needs to be updated.
*
* @param {HTMLOptionElement} option
* @returns {boolean} - True when option has value && if filtering is disabled, combo box has an active selection,
* there is no inputValue, or if option matches user query
*/
const arrayNeedsUpdate = option => option.value && (disableFiltering || isPristine || !inputValue || optionMatchesQuery(option));
/**
* Checks if firstFoundId should be assigned, which is then used to set itemToFocus.
*
* @param {HTMLOptionElement} option
* @return {boolean} - Returns true if filtering is disabled, no firstFoundId is assigned, and the option matches the query.
*/
const isFirstMatch = option => disableFiltering && !firstFoundId && optionMatchesQuery(option);
/**
* Checks if isCurrentSelection should be assigned, which is then used to set itemToFocus.
*
* @param {HTMLOptionElement} option
* @returns {boolean} - Returns true if option.value matches selectEl.value.
*/
const isCurrentSelection = option => selectEl.value && option.value === selectEl.value;
/**
* Update the array of options that should be displayed on the page.
* Assign an ID to each displayed option.
* Identify and assign the option that should receive focus.
*/
optionList.forEach(option => {
if (arrayNeedsUpdate(option)) {
buildOptionsArray(option);
const optionId = `${listOptionBaseId}${options.indexOf(option)}`;
if (isFirstMatch(option)) {
firstFoundId = optionId;
}
if (isCurrentSelection(option)) {
selectedItemId = optionId;
}
}
});
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);
li.setAttribute("aria-selected", ariaSelected);
li.setAttribute("id", optionId);
li.setAttribute("class", classes.join(" "));
li.setAttribute("tabindex", tabindex);
li.setAttribute("role", "option");
li.setAttribute("data-value", option.value);
li.textContent = option.text;
return li;
});
const noResults = document.createElement("li");
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));
} else {
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,
listEl,
statusEl,
focusedOptionEl
} = getComboBoxContext(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,
selectEl,
inputEl
} = getComboBoxContext(listOptionEl);
changeElementValue(selectEl, listOptionEl.dataset.value);
changeElementValue(inputEl, listOptionEl.textContent);
comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS);
hideList(comboBoxEl);
inputEl.focus();
};
/**
* Clear the input of the combo box
*
* @param {HTMLButtonElement} clearButtonEl The clear input button
*/
const clearInput = clearButtonEl => {
const {
comboBoxEl,
listEl,
selectEl,
inputEl
} = getComboBoxContext(clearButtonEl);
const listShown = !listEl.hidden;
if (selectEl.value) changeElementValue(selectEl);
if (inputEl.value) changeElementValue(inputEl);
comboBoxEl.classList.remove(COMBO_BOX_PRISTINE_CLASS);
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,
selectEl,
inputEl
} = 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
* having test that completely matches a list option.
* Otherwise it clears the input and select.
*
* @param {HTMLElement} el An element within the combo box component
*/
const completeSelection = el => {
const {
comboBoxEl,
selectEl,
inputEl,
statusEl
} = 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);
comboBoxEl.classList.add(COMBO_BOX_PRISTINE_CLASS);
return;
}
}
}
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,
inputEl
} = getComboBoxContext(event.target);
hideList(comboBoxEl);
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,
listEl
} = 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 space event from an list option element within the combo box component.
*
* @param {KeyboardEvent} event An event within the combo box component
*/
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,
listEl,
focusedOptionEl
} = getComboBoxContext(event.target);
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;
highlightOption(listOptionEl, 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) {
if (!this.contains(event.relatedTarget)) {
resetSelection(this);
hideList(