@limetech/lime-elements
Version:
413 lines (412 loc) • 13.1 kB
JavaScript
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