UNPKG

@smui/select

Version:

Svelte Material UI - Select

421 lines 18.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 { MDCComponent } from '@smui/base/component'; import { MDCFloatingLabel, } from '@smui/floating-label/component'; import { MDCLineRipple, } from '@smui/line-ripple/component'; import * as menuSurfaceConstants from '@smui/menu-surface/constants'; import { MDCMenu } from '@smui/menu/component'; import * as menuConstants from '@smui/menu/constants'; import { MDCNotchedOutline, } from '@smui/notched-outline/component'; import { MDCRipple } from '@smui/ripple/component'; import { MDCRippleFoundation } from '@smui/ripple/foundation'; import { cssClasses, strings } from './constants'; import { MDCSelectFoundation } from './foundation'; import { MDCSelectHelperText, } from '../helper-text/mdc'; import { MDCSelectIcon } from '../icon/mdc'; /** MDC Select */ export class MDCSelect extends MDCComponent { static attachTo(root) { return new MDCSelect(root); } initialize(labelFactory = (el) => new MDCFloatingLabel(el), lineRippleFactory = (el) => new MDCLineRipple(el), outlineFactory = (el) => new MDCNotchedOutline(el), menuFactory = (el) => new MDCMenu(el), iconFactory = (el) => new MDCSelectIcon(el), helperTextFactory = (el) => new MDCSelectHelperText(el)) { this.selectAnchor = this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR); this.selectedText = this.root.querySelector(strings.SELECTED_TEXT_SELECTOR); this.hiddenInput = this.root.querySelector(strings.HIDDEN_INPUT_SELECTOR); if (!this.selectedText) { throw new Error('MDCSelect: Missing required element: The following selector must be present: ' + `'${strings.SELECTED_TEXT_SELECTOR}'`); } if (this.selectAnchor.hasAttribute(strings.ARIA_CONTROLS)) { const helperTextElement = document.getElementById(this.selectAnchor.getAttribute(strings.ARIA_CONTROLS)); if (helperTextElement) { this.helperText = helperTextFactory(helperTextElement); } } this.menuSetup(menuFactory); const labelElement = this.root.querySelector(strings.LABEL_SELECTOR); this.label = labelElement ? labelFactory(labelElement) : null; const lineRippleElement = this.root.querySelector(strings.LINE_RIPPLE_SELECTOR); this.lineRipple = lineRippleElement ? lineRippleFactory(lineRippleElement) : null; const outlineElement = this.root.querySelector(strings.OUTLINE_SELECTOR); this.outline = outlineElement ? outlineFactory(outlineElement) : null; const leadingIcon = this.root.querySelector(strings.LEADING_ICON_SELECTOR); if (leadingIcon) { this.leadingIcon = iconFactory(leadingIcon); } if (!this.root.classList.contains(cssClasses.OUTLINED)) { this.ripple = this.createRipple(); } } /** * Initializes the select's event listeners and internal state based * on the environment's state. */ initialSyncWithDOM() { this.handleFocus = () => { this.foundation.handleFocus(); }; this.handleBlur = () => { this.foundation.handleBlur(); }; this.handleClick = (event) => { this.selectAnchor.focus(); this.foundation.handleClick(this.getNormalizedXCoordinate(event)); }; this.handleKeydown = (event) => { this.foundation.handleKeydown(event); }; this.handleMenuItemAction = (event) => { this.foundation.handleMenuItemAction(event.detail.index); }; this.handleMenuOpened = () => { this.foundation.handleMenuOpened(); }; this.handleMenuClosed = () => { this.foundation.handleMenuClosed(); }; this.handleMenuClosing = () => { this.foundation.handleMenuClosing(); }; this.selectAnchor.addEventListener('focus', this.handleFocus); this.selectAnchor.addEventListener('blur', this.handleBlur); this.selectAnchor.addEventListener('click', this.handleClick); this.selectAnchor.addEventListener('keydown', this.handleKeydown); this.menu.listen(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed); this.menu.listen(menuSurfaceConstants.strings.CLOSING_EVENT, this.handleMenuClosing); this.menu.listen(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened); this.menu.listen(menuConstants.strings.SELECTED_EVENT, this.handleMenuItemAction); if (this.hiddenInput) { if (this.hiddenInput.value) { // If the hidden input already has a value, use it to restore the // select's value. This can happen e.g. if the user goes back or (in // some browsers) refreshes the page. this.foundation.setValue(this.hiddenInput.value, /** skipNotify */ true); this.foundation.layout(); return; } this.hiddenInput.value = this.value; } } destroy() { this.selectAnchor.removeEventListener('focus', this.handleFocus); this.selectAnchor.removeEventListener('blur', this.handleBlur); this.selectAnchor.removeEventListener('keydown', this.handleKeydown); this.selectAnchor.removeEventListener('click', this.handleClick); this.menu.unlisten(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed); this.menu.unlisten(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened); this.menu.unlisten(menuConstants.strings.SELECTED_EVENT, this.handleMenuItemAction); this.menu.destroy(); if (this.ripple) { this.ripple.destroy(); } if (this.outline) { this.outline.destroy(); } if (this.leadingIcon) { this.leadingIcon.destroy(); } if (this.helperText) { this.helperText.destroy(); } super.destroy(); } get value() { return this.foundation.getValue(); } set value(value) { this.foundation.setValue(value); } setValue(value, skipNotify = false) { this.foundation.setValue(value, skipNotify); } get selectedIndex() { return this.foundation.getSelectedIndex(); } set selectedIndex(selectedIndex) { this.foundation.setSelectedIndex(selectedIndex, /* closeMenu */ true); } setSelectedIndex(selectedIndex, skipNotify = false) { this.foundation.setSelectedIndex(selectedIndex, /* closeMenu */ true, skipNotify); } get disabled() { return this.foundation.getDisabled(); } set disabled(disabled) { this.foundation.setDisabled(disabled); if (this.hiddenInput) { this.hiddenInput.disabled = disabled; } } set leadingIconAriaLabel(label) { this.foundation.setLeadingIconAriaLabel(label); } /** * Sets the text content of the leading icon. */ set leadingIconContent(content) { this.foundation.setLeadingIconContent(content); } /** * Sets the text content of the helper text. */ set helperTextContent(content) { this.foundation.setHelperTextContent(content); } /** * Enables or disables the default validation scheme where a required select * must be non-empty. Set to false for custom validation. * @param useDefaultValidation Set this to false to ignore default * validation scheme. */ set useDefaultValidation(useDefaultValidation) { this.foundation.setUseDefaultValidation(useDefaultValidation); } /** * Sets the current invalid state of the select. */ set valid(isValid) { this.foundation.setValid(isValid); } /** * Checks if the select is in a valid state. */ get valid() { return this.foundation.isValid(); } /** * Sets the control to the required state. */ set required(isRequired) { this.foundation.setRequired(isRequired); } /** * Returns whether the select is required. */ get required() { return this.foundation.getRequired(); } /** * Re-calculates if the notched outline should be notched and if the label * should float. */ layout() { this.foundation.layout(); } /** * Synchronizes the list of options with the state of the foundation. Call * this whenever menu options are dynamically updated. */ layoutOptions() { this.foundation.layoutOptions(); this.menu.layout(); // Update cached menuItemValues for adapter. this.menuItemValues = this.menu.items.map((el) => el.getAttribute(strings.VALUE_ATTR) || ''); if (this.hiddenInput) { this.hiddenInput.value = this.value; } } getDefaultFoundation() { // DO NOT INLINE this variable. For backward compatibility, foundations take // a Partial<MDCFooAdapter>. To ensure we don't accidentally omit any // methods, we need a separate, strongly typed adapter variable. const adapter = Object.assign(Object.assign(Object.assign(Object.assign({}, this.getSelectAdapterMethods()), this.getCommonAdapterMethods()), this.getOutlineAdapterMethods()), this.getLabelAdapterMethods()); return new MDCSelectFoundation(adapter, this.getFoundationMap()); } /** * Handles setup for the menu. */ menuSetup(menuFactory) { this.menuElement = this.root.querySelector(strings.MENU_SELECTOR); this.menu = menuFactory(this.menuElement); this.menu.hasTypeahead = true; this.menu.singleSelection = true; this.menuItemValues = this.menu.items.map((el) => el.getAttribute(strings.VALUE_ATTR) || ''); } createRipple() { // DO NOT INLINE this variable. For backward compatibility, foundations take // a Partial<MDCFooAdapter>. To ensure we don't accidentally omit any // methods, we need a separate, strongly typed adapter variable. // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface. const adapter = Object.assign(Object.assign({}, MDCRipple.createAdapter({ root: this.selectAnchor })), { registerInteractionHandler: (eventType, handler) => { this.selectAnchor.addEventListener(eventType, handler); }, deregisterInteractionHandler: (eventType, handler) => { this.selectAnchor.removeEventListener(eventType, handler); } }); // tslint:enable:object-literal-sort-keys return new MDCRipple(this.selectAnchor, new MDCRippleFoundation(adapter)); } getSelectAdapterMethods() { // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface. return { getMenuItemAttr: (menuItem, attr) => menuItem.getAttribute(attr), setSelectedText: (text) => { this.selectedText.textContent = text; let index = this.menu.selectedIndex; if (index === -1) return; index = index instanceof Array ? index[0] : index; const selectedItem = this.menu.items[index]; if (!selectedItem) return; this.selectedText.setAttribute('aria-label', selectedItem.getAttribute('aria-label') || ''); }, isSelectAnchorFocused: () => document.activeElement === this.selectAnchor, getSelectAnchorAttr: (attr) => this.selectAnchor.getAttribute(attr), setSelectAnchorAttr: (attr, value) => { this.safeSetAttribute(this.selectAnchor, attr, value); }, removeSelectAnchorAttr: (attr) => { this.selectAnchor.removeAttribute(attr); }, addMenuClass: (className) => { this.menuElement.classList.add(className); }, removeMenuClass: (className) => { this.menuElement.classList.remove(className); }, openMenu: () => { this.menu.open = true; }, closeMenu: () => { this.menu.open = false; }, getAnchorElement: () => this.root.querySelector(strings.SELECT_ANCHOR_SELECTOR), setMenuAnchorElement: (anchorEl) => { this.menu.setAnchorElement(anchorEl); }, setMenuAnchorCorner: (anchorCorner) => { this.menu.setAnchorCorner(anchorCorner); }, setMenuWrapFocus: (wrapFocus) => { this.menu.wrapFocus = wrapFocus; }, getSelectedIndex: () => { const index = this.menu.selectedIndex; return index instanceof Array ? index[0] : index; }, setSelectedIndex: (index) => { this.menu.selectedIndex = index; }, focusMenuItemAtIndex: (index) => { var _a; (_a = this.menu.items[index]) === null || _a === void 0 ? void 0 : _a.focus(); }, getMenuItemCount: () => this.menu.items.length, // Cache menu item values. layoutOptions() updates this cache. getMenuItemValues: () => this.menuItemValues, getMenuItemTextAtIndex: (index) => this.menu.getPrimaryTextAtIndex(index), isTypeaheadInProgress: () => this.menu.typeaheadInProgress, typeaheadMatchItem: (nextChar, startingIndex) => this.menu.typeaheadMatchItem(nextChar, startingIndex), }; // tslint:enable:object-literal-sort-keys } getCommonAdapterMethods() { // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface. return { addClass: (className) => { this.root.classList.add(className); }, removeClass: (className) => { this.root.classList.remove(className); }, hasClass: (className) => this.root.classList.contains(className), setRippleCenter: (normalizedX) => { this.lineRipple && this.lineRipple.setRippleCenter(normalizedX); }, activateBottomLine: () => { this.lineRipple && this.lineRipple.activate(); }, deactivateBottomLine: () => { this.lineRipple && this.lineRipple.deactivate(); }, notifyChange: (value) => { if (this.hiddenInput) { this.hiddenInput.value = value; } const index = this.selectedIndex; this.emit(strings.CHANGE_EVENT, { value, index }, true /* shouldBubble */); }, }; // tslint:enable:object-literal-sort-keys } getOutlineAdapterMethods() { // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface. return { hasOutline: () => Boolean(this.outline), notchOutline: (labelWidth) => { this.outline && this.outline.notch(labelWidth); }, closeOutline: () => { this.outline && this.outline.closeNotch(); }, }; // tslint:enable:object-literal-sort-keys } getLabelAdapterMethods() { // tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface. return { hasLabel: () => !!this.label, floatLabel: (shouldFloat) => { this.label && this.label.float(shouldFloat); }, getLabelWidth: () => (this.label ? this.label.getWidth() : 0), setLabelRequired: (isRequired) => { this.label && this.label.setRequired(isRequired); }, }; // tslint:enable:object-literal-sort-keys } /** * Calculates where the line ripple should start based on the x coordinate * within the component. */ getNormalizedXCoordinate(event) { const targetClientRect = event.target.getBoundingClientRect(); const xCoordinate = this.isTouchEvent(event) ? event.touches[0].clientX : event.clientX; return xCoordinate - targetClientRect.left; } isTouchEvent(event) { return Boolean(event.touches); } /** * Returns a map of all subcomponents to subfoundations. */ getFoundationMap() { return { helperText: this.helperText ? this.helperText.foundationForSelect : undefined, leadingIcon: this.leadingIcon ? this.leadingIcon.foundationForSelect : undefined, }; } } //# sourceMappingURL=component.js.map