@limetech/lime-elements
Version:
1,167 lines (1,160 loc) • 68.2 kB
JavaScript
'use strict';
const ponyfill = require('./ponyfill-63966294.js');
/**
* @license
* Copyright 2018 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var _a, _b;
var cssClasses = {
LIST_ITEM_ACTIVATED_CLASS: 'mdc-list-item--activated',
LIST_ITEM_CLASS: 'mdc-list-item',
LIST_ITEM_DISABLED_CLASS: 'mdc-list-item--disabled',
LIST_ITEM_SELECTED_CLASS: 'mdc-list-item--selected',
LIST_ITEM_TEXT_CLASS: 'mdc-list-item__text',
LIST_ITEM_PRIMARY_TEXT_CLASS: 'mdc-list-item__primary-text',
ROOT: 'mdc-list',
};
var evolutionClassNameMap = (_a = {},
_a["" + cssClasses.LIST_ITEM_ACTIVATED_CLASS] = 'mdc-list-item--activated',
_a["" + cssClasses.LIST_ITEM_CLASS] = 'mdc-list-item',
_a["" + cssClasses.LIST_ITEM_DISABLED_CLASS] = 'mdc-list-item--disabled',
_a["" + cssClasses.LIST_ITEM_SELECTED_CLASS] = 'mdc-list-item--selected',
_a["" + cssClasses.LIST_ITEM_PRIMARY_TEXT_CLASS] = 'mdc-list-item__primary-text',
_a["" + cssClasses.ROOT] = 'mdc-list',
_a);
var deprecatedClassNameMap = (_b = {},
_b["" + cssClasses.LIST_ITEM_ACTIVATED_CLASS] = 'mdc-deprecated-list-item--activated',
_b["" + cssClasses.LIST_ITEM_CLASS] = 'mdc-deprecated-list-item',
_b["" + cssClasses.LIST_ITEM_DISABLED_CLASS] = 'mdc-deprecated-list-item--disabled',
_b["" + cssClasses.LIST_ITEM_SELECTED_CLASS] = 'mdc-deprecated-list-item--selected',
_b["" + cssClasses.LIST_ITEM_TEXT_CLASS] = 'mdc-deprecated-list-item__text',
_b["" + cssClasses.LIST_ITEM_PRIMARY_TEXT_CLASS] = 'mdc-deprecated-list-item__primary-text',
_b["" + cssClasses.ROOT] = 'mdc-deprecated-list',
_b);
var strings = {
ACTION_EVENT: 'MDCList:action',
ARIA_CHECKED: 'aria-checked',
ARIA_CHECKED_CHECKBOX_SELECTOR: '[role="checkbox"][aria-checked="true"]',
ARIA_CHECKED_RADIO_SELECTOR: '[role="radio"][aria-checked="true"]',
ARIA_CURRENT: 'aria-current',
ARIA_DISABLED: 'aria-disabled',
ARIA_ORIENTATION: 'aria-orientation',
ARIA_ORIENTATION_HORIZONTAL: 'horizontal',
ARIA_ROLE_CHECKBOX_SELECTOR: '[role="checkbox"]',
ARIA_SELECTED: 'aria-selected',
ARIA_INTERACTIVE_ROLES_SELECTOR: '[role="listbox"], [role="menu"]',
ARIA_MULTI_SELECTABLE_SELECTOR: '[aria-multiselectable="true"]',
CHECKBOX_RADIO_SELECTOR: 'input[type="checkbox"], input[type="radio"]',
CHECKBOX_SELECTOR: 'input[type="checkbox"]',
CHILD_ELEMENTS_TO_TOGGLE_TABINDEX: "\n ." + cssClasses.LIST_ITEM_CLASS + " button:not(:disabled),\n ." + cssClasses.LIST_ITEM_CLASS + " a,\n ." + deprecatedClassNameMap[cssClasses.LIST_ITEM_CLASS] + " button:not(:disabled),\n ." + deprecatedClassNameMap[cssClasses.LIST_ITEM_CLASS] + " a\n ",
DEPRECATED_SELECTOR: '.mdc-deprecated-list',
FOCUSABLE_CHILD_ELEMENTS: "\n ." + cssClasses.LIST_ITEM_CLASS + " button:not(:disabled),\n ." + cssClasses.LIST_ITEM_CLASS + " a,\n ." + cssClasses.LIST_ITEM_CLASS + " input[type=\"radio\"]:not(:disabled),\n ." + cssClasses.LIST_ITEM_CLASS + " input[type=\"checkbox\"]:not(:disabled),\n ." + deprecatedClassNameMap[cssClasses.LIST_ITEM_CLASS] + " button:not(:disabled),\n ." + deprecatedClassNameMap[cssClasses.LIST_ITEM_CLASS] + " a,\n ." + deprecatedClassNameMap[cssClasses.LIST_ITEM_CLASS] + " input[type=\"radio\"]:not(:disabled),\n ." + deprecatedClassNameMap[cssClasses.LIST_ITEM_CLASS] + " input[type=\"checkbox\"]:not(:disabled)\n ",
RADIO_SELECTOR: 'input[type="radio"]',
SELECTED_ITEM_SELECTOR: '[aria-selected="true"], [aria-current="true"]',
};
var numbers = {
UNSET_INDEX: -1,
TYPEAHEAD_BUFFER_CLEAR_TIMEOUT_MS: 300
};
var evolutionAttribute = 'evolution';
/**
* @license
* Copyright 2020 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* KEY provides normalized string values for keys.
*/
var KEY = {
UNKNOWN: 'Unknown',
BACKSPACE: 'Backspace',
ENTER: 'Enter',
SPACEBAR: 'Spacebar',
PAGE_UP: 'PageUp',
PAGE_DOWN: 'PageDown',
END: 'End',
HOME: 'Home',
ARROW_LEFT: 'ArrowLeft',
ARROW_UP: 'ArrowUp',
ARROW_RIGHT: 'ArrowRight',
ARROW_DOWN: 'ArrowDown',
DELETE: 'Delete',
ESCAPE: 'Escape',
TAB: 'Tab',
};
var normalizedKeys = new Set();
// IE11 has no support for new Map with iterable so we need to initialize this
// by hand.
normalizedKeys.add(KEY.BACKSPACE);
normalizedKeys.add(KEY.ENTER);
normalizedKeys.add(KEY.SPACEBAR);
normalizedKeys.add(KEY.PAGE_UP);
normalizedKeys.add(KEY.PAGE_DOWN);
normalizedKeys.add(KEY.END);
normalizedKeys.add(KEY.HOME);
normalizedKeys.add(KEY.ARROW_LEFT);
normalizedKeys.add(KEY.ARROW_UP);
normalizedKeys.add(KEY.ARROW_RIGHT);
normalizedKeys.add(KEY.ARROW_DOWN);
normalizedKeys.add(KEY.DELETE);
normalizedKeys.add(KEY.ESCAPE);
normalizedKeys.add(KEY.TAB);
var KEY_CODE = {
BACKSPACE: 8,
ENTER: 13,
SPACEBAR: 32,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,
HOME: 36,
ARROW_LEFT: 37,
ARROW_UP: 38,
ARROW_RIGHT: 39,
ARROW_DOWN: 40,
DELETE: 46,
ESCAPE: 27,
TAB: 9,
};
var mappedKeyCodes = new Map();
// IE11 has no support for new Map with iterable so we need to initialize this
// by hand.
mappedKeyCodes.set(KEY_CODE.BACKSPACE, KEY.BACKSPACE);
mappedKeyCodes.set(KEY_CODE.ENTER, KEY.ENTER);
mappedKeyCodes.set(KEY_CODE.SPACEBAR, KEY.SPACEBAR);
mappedKeyCodes.set(KEY_CODE.PAGE_UP, KEY.PAGE_UP);
mappedKeyCodes.set(KEY_CODE.PAGE_DOWN, KEY.PAGE_DOWN);
mappedKeyCodes.set(KEY_CODE.END, KEY.END);
mappedKeyCodes.set(KEY_CODE.HOME, KEY.HOME);
mappedKeyCodes.set(KEY_CODE.ARROW_LEFT, KEY.ARROW_LEFT);
mappedKeyCodes.set(KEY_CODE.ARROW_UP, KEY.ARROW_UP);
mappedKeyCodes.set(KEY_CODE.ARROW_RIGHT, KEY.ARROW_RIGHT);
mappedKeyCodes.set(KEY_CODE.ARROW_DOWN, KEY.ARROW_DOWN);
mappedKeyCodes.set(KEY_CODE.DELETE, KEY.DELETE);
mappedKeyCodes.set(KEY_CODE.ESCAPE, KEY.ESCAPE);
mappedKeyCodes.set(KEY_CODE.TAB, KEY.TAB);
var navigationKeys = new Set();
// IE11 has no support for new Set with iterable so we need to initialize this
// by hand.
navigationKeys.add(KEY.PAGE_UP);
navigationKeys.add(KEY.PAGE_DOWN);
navigationKeys.add(KEY.END);
navigationKeys.add(KEY.HOME);
navigationKeys.add(KEY.ARROW_LEFT);
navigationKeys.add(KEY.ARROW_UP);
navigationKeys.add(KEY.ARROW_RIGHT);
navigationKeys.add(KEY.ARROW_DOWN);
/**
* normalizeKey returns the normalized string for a navigational action.
*/
function normalizeKey(evt) {
var key = evt.key;
// If the event already has a normalized key, return it
if (normalizedKeys.has(key)) {
return key;
}
// tslint:disable-next-line:deprecation
var mappedKey = mappedKeyCodes.get(evt.keyCode);
if (mappedKey) {
return mappedKey;
}
return KEY.UNKNOWN;
}
/**
* @license
* Copyright 2020 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var ELEMENTS_KEY_ALLOWED_IN = ['input', 'button', 'textarea', 'select'];
/**
* Ensures that preventDefault is only called if the containing element
* doesn't consume the event, and it will cause an unintended scroll.
*
* @param evt keyboard event to be prevented.
*/
var preventDefaultEvent = function (evt) {
var target = evt.target;
if (!target) {
return;
}
var tagName = ("" + target.tagName).toLowerCase();
if (ELEMENTS_KEY_ALLOWED_IN.indexOf(tagName) === -1) {
evt.preventDefault();
}
};
/**
* @license
* Copyright 2020 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Initializes a state object for typeahead. Use the same reference for calls to
* typeahead functions.
*
* @return The current state of the typeahead process. Each state reference
* represents a typeahead instance as the reference is typically mutated
* in-place.
*/
function initState() {
var state = {
bufferClearTimeout: 0,
currentFirstChar: '',
sortedIndexCursor: 0,
typeaheadBuffer: '',
};
return state;
}
/**
* Initializes typeahead state by indexing the current list items by primary
* text into the sortedIndexByFirstChar data structure.
*
* @param listItemCount numer of items in the list
* @param getPrimaryTextByItemIndex function that returns the primary text at a
* given index
*
* @return Map that maps the first character of the primary text to the full
* list text and it's index
*/
function initSortedIndex(listItemCount, getPrimaryTextByItemIndex) {
var sortedIndexByFirstChar = new Map();
// Aggregate item text to index mapping
for (var i = 0; i < listItemCount; i++) {
var primaryText = getPrimaryTextByItemIndex(i).trim();
if (!primaryText) {
continue;
}
var firstChar = primaryText[0].toLowerCase();
if (!sortedIndexByFirstChar.has(firstChar)) {
sortedIndexByFirstChar.set(firstChar, []);
}
sortedIndexByFirstChar.get(firstChar).push({ text: primaryText.toLowerCase(), index: i });
}
// Sort the mapping
// TODO(b/157162694): Investigate replacing forEach with Map.values()
sortedIndexByFirstChar.forEach(function (values) {
values.sort(function (first, second) {
return first.index - second.index;
});
});
return sortedIndexByFirstChar;
}
/**
* Given the next desired character from the user, it attempts to find the next
* list option matching the buffer. Wraps around if at the end of options.
*
* @param opts Options and accessors
* - nextChar - the next character to match against items
* - sortedIndexByFirstChar - output of `initSortedIndex(...)`
* - focusedItemIndex - the index of the currently focused item
* - focusItemAtIndex - function that focuses a list item at given index
* - skipFocus - whether or not to focus the matched item
* - isItemAtIndexDisabled - function that determines whether an item at a
* given index is disabled
* @param state The typeahead state instance. See `initState`.
*
* @return The index of the matched item, or -1 if no match.
*/
function matchItem(opts, state) {
var nextChar = opts.nextChar, focusItemAtIndex = opts.focusItemAtIndex, sortedIndexByFirstChar = opts.sortedIndexByFirstChar, focusedItemIndex = opts.focusedItemIndex, skipFocus = opts.skipFocus, isItemAtIndexDisabled = opts.isItemAtIndexDisabled;
clearTimeout(state.bufferClearTimeout);
state.bufferClearTimeout = setTimeout(function () {
clearBuffer(state);
}, numbers.TYPEAHEAD_BUFFER_CLEAR_TIMEOUT_MS);
state.typeaheadBuffer = state.typeaheadBuffer + nextChar;
var index;
if (state.typeaheadBuffer.length === 1) {
index = matchFirstChar(sortedIndexByFirstChar, focusedItemIndex, isItemAtIndexDisabled, state);
}
else {
index = matchAllChars(sortedIndexByFirstChar, isItemAtIndexDisabled, state);
}
if (index !== -1 && !skipFocus) {
focusItemAtIndex(index);
}
return index;
}
/**
* Matches the user's single input character in the buffer to the
* next option that begins with such character. Wraps around if at
* end of options. Returns -1 if no match is found.
*/
function matchFirstChar(sortedIndexByFirstChar, focusedItemIndex, isItemAtIndexDisabled, state) {
var firstChar = state.typeaheadBuffer[0];
var itemsMatchingFirstChar = sortedIndexByFirstChar.get(firstChar);
if (!itemsMatchingFirstChar) {
return -1;
}
// Has the same firstChar been recently matched?
// Also, did starting index remain the same between key presses?
// If both hold true, simply increment index.
if (firstChar === state.currentFirstChar &&
itemsMatchingFirstChar[state.sortedIndexCursor].index ===
focusedItemIndex) {
state.sortedIndexCursor =
(state.sortedIndexCursor + 1) % itemsMatchingFirstChar.length;
var newIndex = itemsMatchingFirstChar[state.sortedIndexCursor].index;
if (!isItemAtIndexDisabled(newIndex)) {
return newIndex;
}
}
// If we're here, it means one of the following happened:
// - either firstChar or startingIndex has changed, invalidating the
// cursor.
// - The next item of typeahead is disabled, so we have to look further.
state.currentFirstChar = firstChar;
var newCursorPosition = -1;
var cursorPosition;
// Find the first non-disabled item as a fallback.
for (cursorPosition = 0; cursorPosition < itemsMatchingFirstChar.length; cursorPosition++) {
if (!isItemAtIndexDisabled(itemsMatchingFirstChar[cursorPosition].index)) {
newCursorPosition = cursorPosition;
break;
}
}
// Advance cursor to first item matching the firstChar that is positioned
// after starting item. Cursor is unchanged from fallback if there's no
// such item.
for (; cursorPosition < itemsMatchingFirstChar.length; cursorPosition++) {
if (itemsMatchingFirstChar[cursorPosition].index > focusedItemIndex &&
!isItemAtIndexDisabled(itemsMatchingFirstChar[cursorPosition].index)) {
newCursorPosition = cursorPosition;
break;
}
}
if (newCursorPosition !== -1) {
state.sortedIndexCursor = newCursorPosition;
return itemsMatchingFirstChar[state.sortedIndexCursor].index;
}
return -1;
}
/**
* Attempts to find the next item that matches all of the typeahead buffer.
* Wraps around if at end of options. Returns -1 if no match is found.
*/
function matchAllChars(sortedIndexByFirstChar, isItemAtIndexDisabled, state) {
var firstChar = state.typeaheadBuffer[0];
var itemsMatchingFirstChar = sortedIndexByFirstChar.get(firstChar);
if (!itemsMatchingFirstChar) {
return -1;
}
// Do nothing if text already matches
var startingItem = itemsMatchingFirstChar[state.sortedIndexCursor];
if (startingItem.text.lastIndexOf(state.typeaheadBuffer, 0) === 0 &&
!isItemAtIndexDisabled(startingItem.index)) {
return startingItem.index;
}
// Find next item that matches completely; if no match, we'll eventually
// loop around to same position
var cursorPosition = (state.sortedIndexCursor + 1) % itemsMatchingFirstChar.length;
var nextCursorPosition = -1;
while (cursorPosition !== state.sortedIndexCursor) {
var currentItem = itemsMatchingFirstChar[cursorPosition];
var matches = currentItem.text.lastIndexOf(state.typeaheadBuffer, 0) === 0;
var isEnabled = !isItemAtIndexDisabled(currentItem.index);
if (matches && isEnabled) {
nextCursorPosition = cursorPosition;
break;
}
cursorPosition = (cursorPosition + 1) % itemsMatchingFirstChar.length;
}
if (nextCursorPosition !== -1) {
state.sortedIndexCursor = nextCursorPosition;
return itemsMatchingFirstChar[state.sortedIndexCursor].index;
}
return -1;
}
/**
* Whether or not the given typeahead instaance state is currently typing.
*
* @param state The typeahead state instance. See `initState`.
*/
function isTypingInProgress(state) {
return state.typeaheadBuffer.length > 0;
}
/**
* Clears the typeahaed buffer so that it resets item matching to the first
* character.
*
* @param state The typeahead state instance. See `initState`.
*/
function clearBuffer(state) {
state.typeaheadBuffer = '';
}
/**
* Given a keydown event, it calculates whether or not to automatically focus a
* list item depending on what was typed mimicing the typeahead functionality of
* a standard <select> element that is open.
*
* @param opts Options and accessors
* - event - the KeyboardEvent to handle and parse
* - sortedIndexByFirstChar - output of `initSortedIndex(...)`
* - focusedItemIndex - the index of the currently focused item
* - focusItemAtIndex - function that focuses a list item at given index
* - isItemAtFocusedIndexDisabled - whether or not the currently focused item
* is disabled
* - isTargetListItem - whether or not the event target is a list item
* @param state The typeahead state instance. See `initState`.
*
* @returns index of the item matched by the keydown. -1 if not matched.
*/
function handleKeydown(opts, state) {
var event = opts.event, isTargetListItem = opts.isTargetListItem, focusedItemIndex = opts.focusedItemIndex, focusItemAtIndex = opts.focusItemAtIndex, sortedIndexByFirstChar = opts.sortedIndexByFirstChar, isItemAtIndexDisabled = opts.isItemAtIndexDisabled;
var isArrowLeft = normalizeKey(event) === 'ArrowLeft';
var isArrowUp = normalizeKey(event) === 'ArrowUp';
var isArrowRight = normalizeKey(event) === 'ArrowRight';
var isArrowDown = normalizeKey(event) === 'ArrowDown';
var isHome = normalizeKey(event) === 'Home';
var isEnd = normalizeKey(event) === 'End';
var isEnter = normalizeKey(event) === 'Enter';
var isSpace = normalizeKey(event) === 'Spacebar';
if (event.ctrlKey || event.metaKey || isArrowLeft || isArrowUp ||
isArrowRight || isArrowDown || isHome || isEnd || isEnter) {
return -1;
}
var isCharacterKey = !isSpace && event.key.length === 1;
if (isCharacterKey) {
preventDefaultEvent(event);
var matchItemOpts = {
focusItemAtIndex: focusItemAtIndex,
focusedItemIndex: focusedItemIndex,
nextChar: event.key.toLowerCase(),
sortedIndexByFirstChar: sortedIndexByFirstChar,
skipFocus: false,
isItemAtIndexDisabled: isItemAtIndexDisabled,
};
return matchItem(matchItemOpts, state);
}
if (!isSpace) {
return -1;
}
if (isTargetListItem) {
preventDefaultEvent(event);
}
var typeaheadOnListItem = isTargetListItem && isTypingInProgress(state);
if (typeaheadOnListItem) {
var matchItemOpts = {
focusItemAtIndex: focusItemAtIndex,
focusedItemIndex: focusedItemIndex,
nextChar: ' ',
sortedIndexByFirstChar: sortedIndexByFirstChar,
skipFocus: false,
isItemAtIndexDisabled: isItemAtIndexDisabled,
};
// space participates in typeahead matching if in rapid typing mode
return matchItem(matchItemOpts, state);
}
return -1;
}
/**
* @license
* Copyright 2018 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
function isNumberArray(selectedIndex) {
return selectedIndex instanceof Array;
}
var MDCListFoundation = /** @class */ (function (_super) {
ponyfill.__extends(MDCListFoundation, _super);
function MDCListFoundation(adapter) {
var _this = _super.call(this, ponyfill.__assign(ponyfill.__assign({}, MDCListFoundation.defaultAdapter), adapter)) || this;
_this.wrapFocus = false;
_this.isVertical = true;
_this.isSingleSelectionList = false;
_this.selectedIndex = numbers.UNSET_INDEX;
_this.focusedItemIndex = numbers.UNSET_INDEX;
_this.useActivatedClass = false;
_this.useSelectedAttr = false;
_this.ariaCurrentAttrValue = null;
_this.isCheckboxList = false;
_this.isRadioList = false;
_this.hasTypeahead = false;
// Transiently holds current typeahead prefix from user.
_this.typeaheadState = initState();
_this.sortedIndexByFirstChar = new Map();
return _this;
}
Object.defineProperty(MDCListFoundation, "strings", {
get: function () {
return strings;
},
enumerable: false,
configurable: true
});
Object.defineProperty(MDCListFoundation, "cssClasses", {
get: function () {
return cssClasses;
},
enumerable: false,
configurable: true
});
Object.defineProperty(MDCListFoundation, "numbers", {
get: function () {
return numbers;
},
enumerable: false,
configurable: true
});
Object.defineProperty(MDCListFoundation, "defaultAdapter", {
get: function () {
return {
addClassForElementIndex: function () { return undefined; },
focusItemAtIndex: function () { return undefined; },
getAttributeForElementIndex: function () { return null; },
getFocusedElementIndex: function () { return 0; },
getListItemCount: function () { return 0; },
hasCheckboxAtIndex: function () { return false; },
hasRadioAtIndex: function () { return false; },
isCheckboxCheckedAtIndex: function () { return false; },
isFocusInsideList: function () { return false; },
isRootFocused: function () { return false; },
listItemAtIndexHasClass: function () { return false; },
notifyAction: function () { return undefined; },
removeClassForElementIndex: function () { return undefined; },
setAttributeForElementIndex: function () { return undefined; },
setCheckedCheckboxOrRadioAtIndex: function () { return undefined; },
setTabIndexForListItemChildren: function () { return undefined; },
getPrimaryTextAtIndex: function () { return ''; },
};
},
enumerable: false,
configurable: true
});
MDCListFoundation.prototype.layout = function () {
if (this.adapter.getListItemCount() === 0) {
return;
}
// TODO(b/172274142): consider all items when determining the list's type.
if (this.adapter.hasCheckboxAtIndex(0)) {
this.isCheckboxList = true;
}
else if (this.adapter.hasRadioAtIndex(0)) {
this.isRadioList = true;
}
else {
this.maybeInitializeSingleSelection();
}
if (this.hasTypeahead) {
this.sortedIndexByFirstChar = this.typeaheadInitSortedIndex();
}
};
/** Returns the index of the item that was last focused. */
MDCListFoundation.prototype.getFocusedItemIndex = function () {
return this.focusedItemIndex;
};
/** Toggles focus wrapping with keyboard navigation. */
MDCListFoundation.prototype.setWrapFocus = function (value) {
this.wrapFocus = value;
};
/**
* Toggles orientation direction for keyboard navigation (true for vertical,
* false for horizontal).
*/
MDCListFoundation.prototype.setVerticalOrientation = function (value) {
this.isVertical = value;
};
/** Toggles single-selection behavior. */
MDCListFoundation.prototype.setSingleSelection = function (value) {
this.isSingleSelectionList = value;
if (value) {
this.maybeInitializeSingleSelection();
this.selectedIndex = this.getSelectedIndexFromDOM();
}
};
/**
* Automatically determines whether the list is single selection list. If so,
* initializes the internal state to match the selected item.
*/
MDCListFoundation.prototype.maybeInitializeSingleSelection = function () {
var selectedItemIndex = this.getSelectedIndexFromDOM();
if (selectedItemIndex === numbers.UNSET_INDEX)
return;
var hasActivatedClass = this.adapter.listItemAtIndexHasClass(selectedItemIndex, cssClasses.LIST_ITEM_ACTIVATED_CLASS);
if (hasActivatedClass) {
this.setUseActivatedClass(true);
}
this.isSingleSelectionList = true;
this.selectedIndex = selectedItemIndex;
};
/** @return Index of the first selected item based on the DOM state. */
MDCListFoundation.prototype.getSelectedIndexFromDOM = function () {
var selectedIndex = numbers.UNSET_INDEX;
var listItemsCount = this.adapter.getListItemCount();
for (var i = 0; i < listItemsCount; i++) {
var hasSelectedClass = this.adapter.listItemAtIndexHasClass(i, cssClasses.LIST_ITEM_SELECTED_CLASS);
var hasActivatedClass = this.adapter.listItemAtIndexHasClass(i, cssClasses.LIST_ITEM_ACTIVATED_CLASS);
if (!(hasSelectedClass || hasActivatedClass)) {
continue;
}
selectedIndex = i;
break;
}
return selectedIndex;
};
/**
* Sets whether typeahead is enabled on the list.
* @param hasTypeahead Whether typeahead is enabled.
*/
MDCListFoundation.prototype.setHasTypeahead = function (hasTypeahead) {
this.hasTypeahead = hasTypeahead;
if (hasTypeahead) {
this.sortedIndexByFirstChar = this.typeaheadInitSortedIndex();
}
};
/**
* @return Whether typeahead is currently matching a user-specified prefix.
*/
MDCListFoundation.prototype.isTypeaheadInProgress = function () {
return this.hasTypeahead &&
isTypingInProgress(this.typeaheadState);
};
/** Toggle use of the "activated" CSS class. */
MDCListFoundation.prototype.setUseActivatedClass = function (useActivated) {
this.useActivatedClass = useActivated;
};
/**
* Toggles use of the selected attribute (true for aria-selected, false for
* aria-checked).
*/
MDCListFoundation.prototype.setUseSelectedAttribute = function (useSelected) {
this.useSelectedAttr = useSelected;
};
MDCListFoundation.prototype.getSelectedIndex = function () {
return this.selectedIndex;
};
MDCListFoundation.prototype.setSelectedIndex = function (index, _a) {
var _b = _a === void 0 ? {} : _a, forceUpdate = _b.forceUpdate;
if (!this.isIndexValid(index)) {
return;
}
if (this.isCheckboxList) {
this.setCheckboxAtIndex(index);
}
else if (this.isRadioList) {
this.setRadioAtIndex(index);
}
else {
this.setSingleSelectionAtIndex(index, { forceUpdate: forceUpdate });
}
};
/**
* Focus in handler for the list items.
*/
MDCListFoundation.prototype.handleFocusIn = function (listItemIndex) {
if (listItemIndex >= 0) {
this.focusedItemIndex = listItemIndex;
this.adapter.setAttributeForElementIndex(listItemIndex, 'tabindex', '0');
this.adapter.setTabIndexForListItemChildren(listItemIndex, '0');
}
};
/**
* Focus out handler for the list items.
*/
MDCListFoundation.prototype.handleFocusOut = function (listItemIndex) {
var _this = this;
if (listItemIndex >= 0) {
this.adapter.setAttributeForElementIndex(listItemIndex, 'tabindex', '-1');
this.adapter.setTabIndexForListItemChildren(listItemIndex, '-1');
}
/**
* Between Focusout & Focusin some browsers do not have focus on any
* element. Setting a delay to wait till the focus is moved to next element.
*/
setTimeout(function () {
if (!_this.adapter.isFocusInsideList()) {
_this.setTabindexToFirstSelectedOrFocusedItem();
}
}, 0);
};
/**
* Key handler for the list.
*/
MDCListFoundation.prototype.handleKeydown = function (event, isRootListItem, listItemIndex) {
var _this = this;
var isArrowLeft = normalizeKey(event) === 'ArrowLeft';
var isArrowUp = normalizeKey(event) === 'ArrowUp';
var isArrowRight = normalizeKey(event) === 'ArrowRight';
var isArrowDown = normalizeKey(event) === 'ArrowDown';
var isHome = normalizeKey(event) === 'Home';
var isEnd = normalizeKey(event) === 'End';
var isEnter = normalizeKey(event) === 'Enter';
var isSpace = normalizeKey(event) === 'Spacebar';
// Have to check both upper and lower case, because having caps lock on
// affects the value.
var isLetterA = event.key === 'A' || event.key === 'a';
if (this.adapter.isRootFocused()) {
if (isArrowUp || isEnd) {
event.preventDefault();
this.focusLastElement();
}
else if (isArrowDown || isHome) {
event.preventDefault();
this.focusFirstElement();
}
if (this.hasTypeahead) {
var handleKeydownOpts = {
event: event,
focusItemAtIndex: function (index) {
_this.focusItemAtIndex(index);
},
focusedItemIndex: -1,
isTargetListItem: isRootListItem,
sortedIndexByFirstChar: this.sortedIndexByFirstChar,
isItemAtIndexDisabled: function (index) {
return _this.adapter.listItemAtIndexHasClass(index, cssClasses.LIST_ITEM_DISABLED_CLASS);
},
};
handleKeydown(handleKeydownOpts, this.typeaheadState);
}
return;
}
var currentIndex = this.adapter.getFocusedElementIndex();
if (currentIndex === -1) {
currentIndex = listItemIndex;
if (currentIndex < 0) {
// If this event doesn't have a mdc-list-item ancestor from the
// current list (not from a sublist), return early.
return;
}
}
if ((this.isVertical && isArrowDown) ||
(!this.isVertical && isArrowRight)) {
preventDefaultEvent(event);
this.focusNextElement(currentIndex);
}
else if ((this.isVertical && isArrowUp) || (!this.isVertical && isArrowLeft)) {
preventDefaultEvent(event);
this.focusPrevElement(currentIndex);
}
else if (isHome) {
preventDefaultEvent(event);
this.focusFirstElement();
}
else if (isEnd) {
preventDefaultEvent(event);
this.focusLastElement();
}
else if (isLetterA && event.ctrlKey && this.isCheckboxList) {
event.preventDefault();
this.toggleAll(this.selectedIndex === numbers.UNSET_INDEX ?
[] :
this.selectedIndex);
}
else if (isEnter || isSpace) {
if (isRootListItem) {
// Return early if enter key is pressed on anchor element which triggers
// synthetic MouseEvent event.
var target = event.target;
if (target && target.tagName === 'A' && isEnter) {
return;
}
preventDefaultEvent(event);
if (this.adapter.listItemAtIndexHasClass(currentIndex, cssClasses.LIST_ITEM_DISABLED_CLASS)) {
return;
}
if (!this.isTypeaheadInProgress()) {
if (this.isSelectableList()) {
this.setSelectedIndexOnAction(currentIndex);
}
this.adapter.notifyAction(currentIndex);
}
}
}
if (this.hasTypeahead) {
var handleKeydownOpts = {
event: event,
focusItemAtIndex: function (index) {
_this.focusItemAtIndex(index);
},
focusedItemIndex: this.focusedItemIndex,
isTargetListItem: isRootListItem,
sortedIndexByFirstChar: this.sortedIndexByFirstChar,
isItemAtIndexDisabled: function (index) { return _this.adapter.listItemAtIndexHasClass(index, cssClasses.LIST_ITEM_DISABLED_CLASS); },
};
handleKeydown(handleKeydownOpts, this.typeaheadState);
}
};
/**
* Click handler for the list.
*/
MDCListFoundation.prototype.handleClick = function (index, toggleCheckbox) {
if (index === numbers.UNSET_INDEX) {
return;
}
if (this.adapter.listItemAtIndexHasClass(index, cssClasses.LIST_ITEM_DISABLED_CLASS)) {
return;
}
if (this.isSelectableList()) {
this.setSelectedIndexOnAction(index, toggleCheckbox);
}
this.adapter.notifyAction(index);
};
/**
* Focuses the next element on the list.
*/
MDCListFoundation.prototype.focusNextElement = function (index) {
var count = this.adapter.getListItemCount();
var nextIndex = index + 1;
if (nextIndex >= count) {
if (this.wrapFocus) {
nextIndex = 0;
}
else {
// Return early because last item is already focused.
return index;
}
}
this.focusItemAtIndex(nextIndex);
return nextIndex;
};
/**
* Focuses the previous element on the list.
*/
MDCListFoundation.prototype.focusPrevElement = function (index) {
var prevIndex = index - 1;
if (prevIndex < 0) {
if (this.wrapFocus) {
prevIndex = this.adapter.getListItemCount() - 1;
}
else {
// Return early because first item is already focused.
return index;
}
}
this.focusItemAtIndex(prevIndex);
return prevIndex;
};
MDCListFoundation.prototype.focusFirstElement = function () {
this.focusItemAtIndex(0);
return 0;
};
MDCListFoundation.prototype.focusLastElement = function () {
var lastIndex = this.adapter.getListItemCount() - 1;
this.focusItemAtIndex(lastIndex);
return lastIndex;
};
MDCListFoundation.prototype.focusInitialElement = function () {
var initialIndex = this.getFirstSelectedOrFocusedItemIndex();
this.focusItemAtIndex(initialIndex);
return initialIndex;
};
/**
* @param itemIndex Index of the list item
* @param isEnabled Sets the list item to enabled or disabled.
*/
MDCListFoundation.prototype.setEnabled = function (itemIndex, isEnabled) {
if (!this.isIndexValid(itemIndex)) {
return;
}
if (isEnabled) {
this.adapter.removeClassForElementIndex(itemIndex, cssClasses.LIST_ITEM_DISABLED_CLASS);
this.adapter.setAttributeForElementIndex(itemIndex, strings.ARIA_DISABLED, 'false');
}
else {
this.adapter.addClassForElementIndex(itemIndex, cssClasses.LIST_ITEM_DISABLED_CLASS);
this.adapter.setAttributeForElementIndex(itemIndex, strings.ARIA_DISABLED, 'true');
}
};
MDCListFoundation.prototype.setSingleSelectionAtIndex = function (index, _a) {
var _b = _a === void 0 ? {} : _a, forceUpdate = _b.forceUpdate;
if (this.selectedIndex === index && !forceUpdate) {
return;
}
var selectedClassName = cssClasses.LIST_ITEM_SELECTED_CLASS;
if (this.useActivatedClass) {
selectedClassName = cssClasses.LIST_ITEM_ACTIVATED_CLASS;
}
if (this.selectedIndex !== numbers.UNSET_INDEX) {
this.adapter.removeClassForElementIndex(this.selectedIndex, selectedClassName);
}
this.setAriaForSingleSelectionAtIndex(index);
this.setTabindexAtIndex(index);
if (index !== numbers.UNSET_INDEX) {
this.adapter.addClassForElementIndex(index, selectedClassName);
}
this.selectedIndex = index;
};
/**
* Sets aria attribute for single selection at given index.
*/
MDCListFoundation.prototype.setAriaForSingleSelectionAtIndex = function (index) {
// Detect the presence of aria-current and get the value only during list
// initialization when it is in unset state.
if (this.selectedIndex === numbers.UNSET_INDEX) {
this.ariaCurrentAttrValue =
this.adapter.getAttributeForElementIndex(index, strings.ARIA_CURRENT);
}
var isAriaCurrent = this.ariaCurrentAttrValue !== null;
var ariaAttribute = isAriaCurrent ? strings.ARIA_CURRENT : strings.ARIA_SELECTED;
if (this.selectedIndex !== numbers.UNSET_INDEX) {
this.adapter.setAttributeForElementIndex(this.selectedIndex, ariaAttribute, 'false');
}
if (index !== numbers.UNSET_INDEX) {
var ariaAttributeValue = isAriaCurrent ? this.ariaCurrentAttrValue : 'true';
this.adapter.setAttributeForElementIndex(index, ariaAttribute, ariaAttributeValue);
}
};
/**
* Returns the attribute to use for indicating selection status.
*/
MDCListFoundation.prototype.getSelectionAttribute = function () {
return this.useSelectedAttr ? strings.ARIA_SELECTED : strings.ARIA_CHECKED;
};
/**
* Toggles radio at give index. Radio doesn't change the checked state if it
* is already checked.
*/
MDCListFoundation.prototype.setRadioAtIndex = function (index) {
var selectionAttribute = this.getSelectionAttribute();
this.adapter.setCheckedCheckboxOrRadioAtIndex(index, true);
if (this.selectedIndex !== numbers.UNSET_INDEX) {
this.adapter.setAttributeForElementIndex(this.selectedIndex, selectionAttribute, 'false');
}
this.adapter.setAttributeForElementIndex(index, selectionAttribute, 'true');
this.selectedIndex = index;
};
MDCListFoundation.prototype.setCheckboxAtIndex = function (index) {
var selectionAttribute = this.getSelectionAttribute();
for (var i = 0; i < this.adapter.getListItemCount(); i++) {
var isChecked = false;
if (index.indexOf(i) >= 0) {
isChecked = true;
}
this.adapter.setCheckedCheckboxOrRadioAtIndex(i, isChecked);
this.adapter.setAttributeForElementIndex(i, selectionAttribute, isChecked ? 'true' : 'false');
}
this.selectedIndex = index;
};
MDCListFoundation.prototype.setTabindexAtIndex = function (index) {
if (this.focusedItemIndex === numbers.UNSET_INDEX && index !== 0) {
// If some list item was selected set first list item's tabindex to -1.
// Generally, tabindex is set to 0 on first list item of list that has no
// preselected items.
this.adapter.setAttributeForElementIndex(0, 'tabindex', '-1');
}
else if (this.focusedItemIndex >= 0 && this.focusedItemIndex !== index) {
this.adapter.setAttributeForElementIndex(this.focusedItemIndex, 'tabindex', '-1');
}
// Set the previous selection's tabindex to -1. We need this because
// in selection menus that are not visible, programmatically setting an
// option will not change focus but will change where tabindex should be 0.
if (!(this.selectedIndex instanceof Array) &&
this.selectedIndex !== index) {
this.adapter.setAttributeForElementIndex(this.selectedIndex, 'tabindex', '-1');
}
if (index !== numbers.UNSET_INDEX) {
this.adapter.setAttributeForElementIndex(index, 'tabindex', '0');
}
};
/**
* @return Return true if it is single selectin list, checkbox list or radio
* list.
*/
MDCListFoundation.prototype.isSelectableList = function () {
return this.isSingleSelectionList || this.isCheckboxList ||
this.isRadioList;
};
MDCListFoundation.prototype.setTabindexToFirstSelectedOrFocusedItem = function () {
var targetIndex = this.getFirstSelectedOrFocusedItemIndex();
this.setTabindexAtIndex(targetIndex);
};
MDCListFoundation.prototype.getFirstSelectedOrFocusedItemIndex = function () {
// Action lists retain focus on the most recently focused item.
if (!this.isSelectableList()) {
return Math.max(this.focusedItemIndex, 0);
}
// Single-selection lists focus the selected item.
if (typeof this.selectedIndex === 'number' &&
this.selectedIndex !== numbers.UNSET_INDEX) {
return this.selectedIndex;
}
// Multiple-selection lists focus the first selected item.
if (isNumberArray(this.selectedIndex) && this.selectedIndex.length > 0) {
return this.selectedIndex.reduce(function (minIndex, currentIndex) { return Math.min(minIndex, currentIndex); });
}
// Selection lists without a selection focus the first item.
return 0;
};
MDCListFoundation.prototype.isIndexValid = function (index) {
var _this = this;
if (index instanceof Array) {
if (!this.isCheckboxList) {
throw new Error('MDCListFoundation: Array of index is only supported for checkbox based list');
}
if (index.length === 0) {
return true;
}
else {
return index.some(function (i) { return _this.isIndexInRange(i); });
}
}
else if (typeof index === 'number') {
if (this.isCheckboxList) {
throw new Error("MDCListFoundation: Expected array of index for checkbox based list but got number: " + index);
}
return this.isIndexInRange(index) ||
this.isSingleSelectionList && index === numbers.UNSET_INDEX;
}
else {
return false;
}
};
MDCListFoundation.prototype.isIndexInRange = function (index) {
var listSize = this.adapter.getListItemCount();
return index >= 0 && index < listSize;
};
/**
* Sets selected index on user action, toggles checkbox / radio based on
* toggleCheckbox value. User interaction should not toggle list item(s) when
* disabled.
*/
MDCListFoundation.prototype.setSelectedIndexOnAction = function (index, toggleCheckbox) {
if (toggleCheckbox === void 0) { toggleCheckbox = true; }
if (this.isCheckboxList) {
this.toggleCheckboxAtIndex(index, toggleCheckbox);
}
else {
this.setSelectedIndex(index);
}
};
MDCListFoundation.prototype.toggleCheckboxAtIndex = function (index, toggleCheckbox) {
var selectionAttribute = this.getSelectionAttribute();
var isChecked = this.adapter.isCheckboxCheckedAtIndex(index);
if (toggleCheckbox) {
isChecked = !isChecked;
this.adapter.setCheckedCheckboxOrRadioAtIndex(index, isChecked);
}
this.adapter.setAttributeForElementIndex(index, selectionAttribute, isChecked ? 'true' : 'false');
// If none of the checkbox items are selected and selectedIndex is not
// initialized then provide a default value.
var selectedIndexes = this.selectedIndex === numbers.UNSET_INDEX ?
[] :
this.selectedIndex.slice();
if (isChecked) {
selectedIndexes.push(index);
}
else {
selectedIndexes = selectedIndexes.filter(function (i) { return i !== index; });
}
this.selectedIndex = selectedIndexes;
};
MDCListFoundation.prototype.focusItemAtIndex = function (index) {
this.adapter.focusItemAtIndex(index);
this.focusedItemIndex = index;
};
MDCListFoundation.prototype.toggleAll = function (currentlySelectedIndexes) {
var count = this.adapter.getListItemCount();
// If all items are selected, deselect everything.
if (currentlySelectedIndexes.length === count) {
this.setCheckboxAtIndex([]);
}
else {
// Otherwise select all enabled options.
var allIndexes = [];
for (var i = 0; i < count; i++) {
if (!this.adapter.listItemAtIndexHasClass(i, cssClasses.LIST_ITEM_DISABLED_CLASS) ||
currentlySelectedIndexes.indexOf(i) > -1) {
allIndexes.push(i);
}
}
this.setCheckboxAtIndex(allIndexes);
}
};
/**
* Given the next desired character from the user, adds it to the typeahead
* buffer. Then, attempts to find the next option matching the buffer. Wraps
* around if at the end of options.
*
* @param nextChar The next character to add to the prefix buffer.
* @param startingIndex The index from which to start matching. Only relevant
* when starting a new match sequence. To start a new match sequence,
* clear the buffer using `clearTypeaheadBuffer`, or wait for the buffer
* to clear after a set interval defined in list foundation. Defaults to
* the currently focused index.
* @return The index of the matched item, or -1 if no match.
*/
MDCListFoundation.prototype.typeaheadMatchItem = function (nextChar, startingIndex, skipFocus) {