UNPKG

@material/select

Version:

The Material Components web select (text field drop-down) component

448 lines • 19.2 kB
/** * @license * Copyright 2016 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. */ import { __assign, __extends } from "tslib"; import { MDCFoundation } from '@material/base/foundation'; import { KEY, normalizeKey } from '@material/dom/keyboard'; import { Corner } from '@material/menu-surface/constants'; import { cssClasses, numbers, strings } from './constants'; var MDCSelectFoundation = /** @class */ (function (_super) { __extends(MDCSelectFoundation, _super); /* istanbul ignore next: optional argument is not a branch statement */ /** * @param adapter * @param foundationMap Map from subcomponent names to their subfoundations. */ function MDCSelectFoundation(adapter, foundationMap) { if (foundationMap === void 0) { foundationMap = {}; } var _this = _super.call(this, __assign(__assign({}, MDCSelectFoundation.defaultAdapter), adapter)) || this; // Disabled state _this.disabled = false; // isMenuOpen is used to track the state of the menu by listening to the // MDCMenuSurface:closed event For reference, menu.open will return false if // the menu is still closing, but isMenuOpen returns false only after the menu // has closed _this.isMenuOpen = false; // By default, select is invalid if it is required but no value is selected. _this.useDefaultValidation = true; _this.customValidity = true; _this.lastSelectedIndex = numbers.UNSET_INDEX; _this.clickDebounceTimeout = 0; _this.recentlyClicked = false; _this.leadingIcon = foundationMap.leadingIcon; _this.helperText = foundationMap.helperText; return _this; } Object.defineProperty(MDCSelectFoundation, "cssClasses", { get: function () { return cssClasses; }, enumerable: false, configurable: true }); Object.defineProperty(MDCSelectFoundation, "numbers", { get: function () { return numbers; }, enumerable: false, configurable: true }); Object.defineProperty(MDCSelectFoundation, "strings", { get: function () { return strings; }, enumerable: false, configurable: true }); Object.defineProperty(MDCSelectFoundation, "defaultAdapter", { /** * See {@link MDCSelectAdapter} for typing information on parameters and return types. */ get: function () { // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface. return { addClass: function () { return undefined; }, removeClass: function () { return undefined; }, hasClass: function () { return false; }, activateBottomLine: function () { return undefined; }, deactivateBottomLine: function () { return undefined; }, getSelectedIndex: function () { return -1; }, setSelectedIndex: function () { return undefined; }, hasLabel: function () { return false; }, floatLabel: function () { return undefined; }, getLabelWidth: function () { return 0; }, setLabelRequired: function () { return undefined; }, hasOutline: function () { return false; }, notchOutline: function () { return undefined; }, closeOutline: function () { return undefined; }, setRippleCenter: function () { return undefined; }, notifyChange: function () { return undefined; }, setSelectedText: function () { return undefined; }, isSelectAnchorFocused: function () { return false; }, getSelectAnchorAttr: function () { return ''; }, setSelectAnchorAttr: function () { return undefined; }, removeSelectAnchorAttr: function () { return undefined; }, addMenuClass: function () { return undefined; }, removeMenuClass: function () { return undefined; }, openMenu: function () { return undefined; }, closeMenu: function () { return undefined; }, getAnchorElement: function () { return null; }, setMenuAnchorElement: function () { return undefined; }, setMenuAnchorCorner: function () { return undefined; }, setMenuWrapFocus: function () { return undefined; }, focusMenuItemAtIndex: function () { return undefined; }, getMenuItemCount: function () { return 0; }, getMenuItemValues: function () { return []; }, getMenuItemTextAtIndex: function () { return ''; }, isTypeaheadInProgress: function () { return false; }, typeaheadMatchItem: function () { return -1; }, }; // tslint:enable:object-literal-sort-keys }, enumerable: false, configurable: true }); /** Returns the index of the currently selected menu item, or -1 if none. */ MDCSelectFoundation.prototype.getSelectedIndex = function () { return this.adapter.getSelectedIndex(); }; MDCSelectFoundation.prototype.setSelectedIndex = function (index, closeMenu, skipNotify) { if (closeMenu === void 0) { closeMenu = false; } if (skipNotify === void 0) { skipNotify = false; } if (index >= this.adapter.getMenuItemCount()) { return; } if (index === numbers.UNSET_INDEX) { this.adapter.setSelectedText(''); } else { this.adapter.setSelectedText(this.adapter.getMenuItemTextAtIndex(index).trim()); } this.adapter.setSelectedIndex(index); if (closeMenu) { this.adapter.closeMenu(); } if (!skipNotify && this.lastSelectedIndex !== index) { this.handleChange(); } this.lastSelectedIndex = index; }; MDCSelectFoundation.prototype.setValue = function (value, skipNotify) { if (skipNotify === void 0) { skipNotify = false; } var index = this.adapter.getMenuItemValues().indexOf(value); this.setSelectedIndex(index, /** closeMenu */ false, skipNotify); }; MDCSelectFoundation.prototype.getValue = function () { var index = this.adapter.getSelectedIndex(); var menuItemValues = this.adapter.getMenuItemValues(); return index !== numbers.UNSET_INDEX ? menuItemValues[index] : ''; }; MDCSelectFoundation.prototype.getDisabled = function () { return this.disabled; }; MDCSelectFoundation.prototype.setDisabled = function (isDisabled) { this.disabled = isDisabled; if (this.disabled) { this.adapter.addClass(cssClasses.DISABLED); this.adapter.closeMenu(); } else { this.adapter.removeClass(cssClasses.DISABLED); } if (this.leadingIcon) { this.leadingIcon.setDisabled(this.disabled); } if (this.disabled) { // Prevent click events from focusing select. Simply pointer-events: none // is not enough since screenreader clicks may bypass this. this.adapter.removeSelectAnchorAttr('tabindex'); } else { this.adapter.setSelectAnchorAttr('tabindex', '0'); } this.adapter.setSelectAnchorAttr('aria-disabled', this.disabled.toString()); }; /** Opens the menu. */ MDCSelectFoundation.prototype.openMenu = function () { this.adapter.addClass(cssClasses.ACTIVATED); this.adapter.openMenu(); this.isMenuOpen = true; this.adapter.setSelectAnchorAttr('aria-expanded', 'true'); }; /** * @param content Sets the content of the helper text. */ MDCSelectFoundation.prototype.setHelperTextContent = function (content) { if (this.helperText) { this.helperText.setContent(content); } }; /** * Re-calculates if the notched outline should be notched and if the label * should float. */ MDCSelectFoundation.prototype.layout = function () { if (this.adapter.hasLabel()) { var optionHasValue = this.getValue().length > 0; var isFocused = this.adapter.hasClass(cssClasses.FOCUSED); var shouldFloatAndNotch = optionHasValue || isFocused; var isRequired = this.adapter.hasClass(cssClasses.REQUIRED); this.notchOutline(shouldFloatAndNotch); this.adapter.floatLabel(shouldFloatAndNotch); this.adapter.setLabelRequired(isRequired); } }; /** * Synchronizes the list of options with the state of the foundation. Call * this whenever menu options are dynamically updated. */ MDCSelectFoundation.prototype.layoutOptions = function () { var menuItemValues = this.adapter.getMenuItemValues(); var selectedIndex = menuItemValues.indexOf(this.getValue()); this.setSelectedIndex(selectedIndex, /** closeMenu */ false, /** skipNotify */ true); }; MDCSelectFoundation.prototype.handleMenuOpened = function () { if (this.adapter.getMenuItemValues().length === 0) { return; } // Menu should open to the last selected element, should open to first menu item otherwise. var selectedIndex = this.getSelectedIndex(); var focusItemIndex = selectedIndex >= 0 ? selectedIndex : 0; this.adapter.focusMenuItemAtIndex(focusItemIndex); }; MDCSelectFoundation.prototype.handleMenuClosing = function () { this.adapter.setSelectAnchorAttr('aria-expanded', 'false'); }; MDCSelectFoundation.prototype.handleMenuClosed = function () { this.adapter.removeClass(cssClasses.ACTIVATED); this.isMenuOpen = false; // Unfocus the select if menu is closed without a selection if (!this.adapter.isSelectAnchorFocused()) { this.blur(); } }; /** * Handles value changes, via change event or programmatic updates. */ MDCSelectFoundation.prototype.handleChange = function () { this.layout(); this.adapter.notifyChange(this.getValue()); var isRequired = this.adapter.hasClass(cssClasses.REQUIRED); if (isRequired && this.useDefaultValidation) { this.setValid(this.isValid()); } }; MDCSelectFoundation.prototype.handleMenuItemAction = function (index) { this.setSelectedIndex(index, /** closeMenu */ true); }; /** * Handles focus events from select element. */ MDCSelectFoundation.prototype.handleFocus = function () { this.adapter.addClass(cssClasses.FOCUSED); this.layout(); this.adapter.activateBottomLine(); }; /** * Handles blur events from select element. */ MDCSelectFoundation.prototype.handleBlur = function () { if (this.isMenuOpen) { return; } this.blur(); }; MDCSelectFoundation.prototype.handleClick = function (normalizedX) { if (this.disabled || this.recentlyClicked) { return; } this.setClickDebounceTimeout(); if (this.isMenuOpen) { this.adapter.closeMenu(); return; } this.adapter.setRippleCenter(normalizedX); this.openMenu(); }; /** * Handles keydown events on select element. Depending on the type of * character typed, does typeahead matching or opens menu. */ MDCSelectFoundation.prototype.handleKeydown = function (event) { if (this.isMenuOpen || !this.adapter.hasClass(cssClasses.FOCUSED)) { return; } var isEnter = normalizeKey(event) === KEY.ENTER; var isSpace = normalizeKey(event) === KEY.SPACEBAR; var arrowUp = normalizeKey(event) === KEY.ARROW_UP; var arrowDown = normalizeKey(event) === KEY.ARROW_DOWN; var isModifier = event.ctrlKey || event.metaKey; // Typeahead if (!isModifier && (!isSpace && event.key && event.key.length === 1 || isSpace && this.adapter.isTypeaheadInProgress())) { var key = isSpace ? ' ' : event.key; var typeaheadNextIndex = this.adapter.typeaheadMatchItem(key, this.getSelectedIndex()); if (typeaheadNextIndex >= 0) { this.setSelectedIndex(typeaheadNextIndex); } event.preventDefault(); return; } if (!isEnter && !isSpace && !arrowUp && !arrowDown) { return; } this.openMenu(); event.preventDefault(); }; /** * Opens/closes the notched outline. */ MDCSelectFoundation.prototype.notchOutline = function (openNotch) { if (!this.adapter.hasOutline()) { return; } var isFocused = this.adapter.hasClass(cssClasses.FOCUSED); if (openNotch) { var labelScale = numbers.LABEL_SCALE; var labelWidth = this.adapter.getLabelWidth() * labelScale; this.adapter.notchOutline(labelWidth); } else if (!isFocused) { this.adapter.closeOutline(); } }; /** * Sets the aria label of the leading icon. */ MDCSelectFoundation.prototype.setLeadingIconAriaLabel = function (label) { if (this.leadingIcon) { this.leadingIcon.setAriaLabel(label); } }; /** * Sets the text content of the leading icon. */ MDCSelectFoundation.prototype.setLeadingIconContent = function (content) { if (this.leadingIcon) { this.leadingIcon.setContent(content); } }; MDCSelectFoundation.prototype.getUseDefaultValidation = function () { return this.useDefaultValidation; }; MDCSelectFoundation.prototype.setUseDefaultValidation = function (useDefaultValidation) { this.useDefaultValidation = useDefaultValidation; }; MDCSelectFoundation.prototype.setValid = function (isValid) { if (!this.useDefaultValidation) { this.customValidity = isValid; } this.adapter.setSelectAnchorAttr('aria-invalid', (!isValid).toString()); if (isValid) { this.adapter.removeClass(cssClasses.INVALID); this.adapter.removeMenuClass(cssClasses.MENU_INVALID); } else { this.adapter.addClass(cssClasses.INVALID); this.adapter.addMenuClass(cssClasses.MENU_INVALID); } this.syncHelperTextValidity(isValid); }; MDCSelectFoundation.prototype.isValid = function () { if (this.useDefaultValidation && this.adapter.hasClass(cssClasses.REQUIRED) && !this.adapter.hasClass(cssClasses.DISABLED)) { // See notes for required attribute under https://www.w3.org/TR/html52/sec-forms.html#the-select-element // TL;DR: Invalid if no index is selected, or if the first index is selected and has an empty value. return this.getSelectedIndex() !== numbers.UNSET_INDEX && (this.getSelectedIndex() !== 0 || Boolean(this.getValue())); } return this.customValidity; }; MDCSelectFoundation.prototype.setRequired = function (isRequired) { if (isRequired) { this.adapter.addClass(cssClasses.REQUIRED); } else { this.adapter.removeClass(cssClasses.REQUIRED); } this.adapter.setSelectAnchorAttr('aria-required', isRequired.toString()); this.adapter.setLabelRequired(isRequired); }; MDCSelectFoundation.prototype.getRequired = function () { return this.adapter.getSelectAnchorAttr('aria-required') === 'true'; }; MDCSelectFoundation.prototype.init = function () { var anchorEl = this.adapter.getAnchorElement(); if (anchorEl) { this.adapter.setMenuAnchorElement(anchorEl); this.adapter.setMenuAnchorCorner(Corner.BOTTOM_START); } this.adapter.setMenuWrapFocus(false); this.setDisabled(this.adapter.hasClass(cssClasses.DISABLED)); this.syncHelperTextValidity(!this.adapter.hasClass(cssClasses.INVALID)); this.layout(); this.layoutOptions(); }; /** * Unfocuses the select component. */ MDCSelectFoundation.prototype.blur = function () { this.adapter.removeClass(cssClasses.FOCUSED); this.layout(); this.adapter.deactivateBottomLine(); var isRequired = this.adapter.hasClass(cssClasses.REQUIRED); if (isRequired && this.useDefaultValidation) { this.setValid(this.isValid()); } }; MDCSelectFoundation.prototype.syncHelperTextValidity = function (isValid) { if (!this.helperText) { return; } this.helperText.setValidity(isValid); var helperTextVisible = this.helperText.isVisible(); var helperTextId = this.helperText.getId(); if (helperTextVisible && helperTextId) { this.adapter.setSelectAnchorAttr(strings.ARIA_DESCRIBEDBY, helperTextId); } else { // Needed because screenreaders will read labels pointed to by // `aria-describedby` even if they are `aria-hidden`. this.adapter.removeSelectAnchorAttr(strings.ARIA_DESCRIBEDBY); } }; MDCSelectFoundation.prototype.setClickDebounceTimeout = function () { var _this = this; clearTimeout(this.clickDebounceTimeout); this.clickDebounceTimeout = setTimeout(function () { _this.recentlyClicked = false; }, numbers.CLICK_DEBOUNCE_TIMEOUT_MS); this.recentlyClicked = true; }; return MDCSelectFoundation; }(MDCFoundation)); export { MDCSelectFoundation }; // tslint:disable-next-line:no-default-export Needed for backward compatibility with MDC Web v0.44.0 and earlier. export default MDCSelectFoundation; //# sourceMappingURL=foundation.js.map