UNPKG

@limetech/lime-elements

Version:
413 lines (412 loc) • 13.1 kB
import { MDCFloatingLabel } from '@material/floating-label'; import { MDCSelectHelperText } from '@material/select/helper-text'; import { h, } from '@stencil/core'; import { isMobileDevice } from '../../util/device'; import { ENTER, SPACE } from '../../util/keycodes'; import { isMultiple } from '../../util/multiple'; import { createRandomString } from '../../util/random-string'; import { SelectTemplate, triggerIconColorWarning } from './select.template'; /** * @exampleComponent limel-example-select * @exampleComponent limel-example-select-with-icons * @exampleComponent limel-example-select-with-separators * @exampleComponent limel-example-select-with-secondary-text * @exampleComponent limel-example-select-multiple * @exampleComponent limel-example-select-with-empty-option * @exampleComponent limel-example-select-preselected * @exampleComponent limel-example-select-change-options * @exampleComponent limel-example-select-dialog */ export class Select { constructor() { this.hasChanged = false; this.checkValid = false; this.disabled = false; this.readonly = false; this.invalid = undefined; this.required = false; this.label = undefined; this.helperText = undefined; this.value = undefined; this.options = []; this.multiple = false; this.menuOpen = false; this.handleMenuChange = this.handleMenuChange.bind(this); this.handleNativeChange = this.handleNativeChange.bind(this); this.handleMenuTriggerKeyPress = this.handleMenuTriggerKeyPress.bind(this); this.openMenu = this.openMenu.bind(this); this.closeMenu = this.closeMenu.bind(this); this.portalId = createRandomString(); } connectedCallback() { this.initialize(); } componentWillLoad() { this.isMobileDevice = isMobileDevice(); // It should not be possible to render the native select for consumers, but we still want to make it testable. // We can set this attribute in tests to force rendering of the native select if (Object.hasOwn(this.host.dataset, 'native')) { this.isMobileDevice = true; } } componentDidLoad() { this.initialize(); triggerIconColorWarning(this.getOptionsExcludingSeparators()); } initialize() { let element; element = this.host.shadowRoot.querySelector('.mdc-floating-label'); if (!element) { return; } this.mdcFloatingLabel = new MDCFloatingLabel(element); element = this.host.shadowRoot.querySelector('.mdc-select-helper-text'); if (element) { this.mdcSelectHelperText = new MDCSelectHelperText(element); } } disconnectedCallback() { if (this.mdcFloatingLabel) { this.mdcFloatingLabel.destroy(); } if (this.mdcSelectHelperText) { this.mdcSelectHelperText.destroy(); } } componentDidUpdate() { if (this.menuOpen) { this.setMenuFocus(); } } render() { const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index'); return (h(SelectTemplate, { id: this.portalId, disabled: this.disabled || this.readonly, readonly: this.readonly, required: this.required, invalid: this.invalid, label: this.label, helperText: this.helperText, value: this.value, options: this.options, onMenuChange: this.handleMenuChange, onNativeChange: this.handleNativeChange, onTriggerPress: this.handleMenuTriggerKeyPress, multiple: this.multiple, isOpen: this.menuOpen, open: this.openMenu, close: this.closeMenu, checkValid: this.checkValid, native: this.isMobileDevice, dropdownZIndex: dropdownZIndex })); } watchOpen(newValue, oldValue) { if (this.checkValid) { return; } // Menu was closed for the first time if (!newValue && oldValue) { this.checkValid = true; } } setMenuFocus() { if (this.isMobileDevice) { return; } setTimeout(() => { var _a; const list = document.querySelector(`#${this.portalId} limel-menu-surface limel-list`); const firstItem = (_a = list === null || list === void 0 ? void 0 : list.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('[tabindex]'); if (firstItem) { firstItem.focus(); } }); } setTriggerFocus() { const trigger = this.host.shadowRoot.querySelector('.limel-select-trigger'); trigger.focus(); } handleMenuChange(event) { var _a, _b; event.stopPropagation(); if (isMultiple(event.detail)) { const selector = `#${this.portalId} limel-menu-surface`; const menuSurface = (_b = (_a = document .querySelector(selector)) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.mdc-menu-surface'); const scrollPosition = (menuSurface === null || menuSurface === void 0 ? void 0 : menuSurface.scrollTop) || 0; const listItems = event.detail; const options = listItems.map((item) => item.value); this.change.emit(options); // Using a single requestAnimationFrame or setTimeout doesn't // work. Using two nested `requestAnimationFrame` worked most of // the time, but not always. Using `setTimeout` inside the // `requestAnimationFrame` seems to work consistently. /Ads requestAnimationFrame(() => { setTimeout(() => { menuSurface.scrollTop = scrollPosition; }); }); return; } if (!event.detail.selected) { return; } const listItem = event.detail; const option = listItem.value; if (option.disabled) { return; } this.change.emit(option); this.menuOpen = false; this.setTriggerFocus(); } openMenu() { if (this.emitFirstChangeEvent()) { this.hasChanged = true; this.change.emit(this.getOptionsExcludingSeparators()[0]); } this.menuOpen = true; } emitFirstChangeEvent() { return !this.hasChanged && this.isMobileDevice && !this.value; } closeMenu() { this.menuOpen = false; this.setTriggerFocus(); } handleMenuTriggerKeyPress(event) { const isEnter = event.key === ENTER; const isSpace = event.key === SPACE; if (!this.menuOpen && (isSpace || isEnter)) { event.stopPropagation(); event.preventDefault(); this.menuOpen = true; } } handleNativeChange(event) { event.stopPropagation(); const element = this.host.shadowRoot.querySelector('select.limel-select__native-control'); const options = Array.apply(null, element.options) // eslint-disable-line prefer-spread .filter((optionElement) => { return !!optionElement.selected; }) .map((optionElement) => { return this.getOptionsExcludingSeparators().find((o) => o.value === optionElement.value); }); if (this.multiple) { this.change.emit(options); return; } this.change.emit(options[0]); this.menuOpen = false; } getOptionsExcludingSeparators() { return this.options.filter((option) => !('separator' in option)); } static get is() { return "limel-select"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["select.scss"] }; } static get styleUrls() { return { "$": ["select.css"] }; } static get properties() { return { "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to make the field disabled.\nand visually shows that the `select` component is editable but disabled.\nThis tells the users that if certain requirements are met,\nthe component may become interactable." }, "attribute": "disabled", "reflect": true, "defaultValue": "false" }, "readonly": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to make the field read-only.\nThis visualizes the component slightly differently.\nBut shows no visual sign indicating that the component is disabled\nor can ever become interactable." }, "attribute": "readonly", "reflect": true, "defaultValue": "false" }, "invalid": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to indicate that the current value of the select is\ninvalid." }, "attribute": "invalid", "reflect": true }, "required": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "True if the control requires a value." }, "attribute": "required", "reflect": true, "defaultValue": "false" }, "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Text to display next to the select." }, "attribute": "label", "reflect": true }, "helperText": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Optional helper text to display below the input field when it has focus." }, "attribute": "helper-text", "reflect": true }, "value": { "type": "unknown", "mutable": false, "complexType": { "original": "Option | Option[]", "resolved": "Option<string> | Option<string>[]", "references": { "Option": { "location": "import", "path": "../select/option.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Currently selected value or values.\nIf `multiple` is `true`, this must be an array. Otherwise it must be a\nsingle value." } }, "options": { "type": "unknown", "mutable": false, "complexType": { "original": "Array<Option | ListSeparator>", "resolved": "(ListSeparator | Option<string>)[]", "references": { "Array": { "location": "global" }, "Option": { "location": "import", "path": "../select/option.types" }, "ListSeparator": { "location": "import", "path": "../list/list-item.types" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "List of options." }, "defaultValue": "[]" }, "multiple": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Set to `true` to allow multiple values to be selected." }, "attribute": "multiple", "reflect": false, "defaultValue": "false" } }; } static get states() { return { "menuOpen": {} }; } static get events() { return [{ "method": "change", "name": "change", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emitted when the value is changed." }, "complexType": { "original": "Option | Option[]", "resolved": "Option<string> | Option<string>[]", "references": { "Option": { "location": "import", "path": "../select/option.types" } } } }]; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "menuOpen", "methodName": "watchOpen" }]; } } //# sourceMappingURL=select.js.map