UNPKG

@limetech/lime-elements

Version:
1,167 lines (1,160 loc) • 68.2 kB
'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) {