diff --git a/README.md b/README.md index 1ab27b7..af232f6 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# tp-element +# tp-dropdown diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..141eeb1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,123 @@ +{ + "name": "@tp/tp-dropdown", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@lit/reactive-element": { + "version": "1.3.0", + "resolved": "https://verdaccio.codeblob.work/@lit%2freactive-element/-/reactive-element-1.3.0.tgz", + "integrity": "sha512-0TKSIuJHXNLM0k98fi0AdMIdUoHIYlDHTP+0Vruc2SOs4T6vU1FinXgSvYd8mSrkt+8R+qdRAXvjpqrMXMyBgw==" + }, + "@tp/helpers": { + "version": "1.0.1", + "resolved": "https://verdaccio.codeblob.work/@tp%2fhelpers/-/helpers-1.0.1.tgz", + "integrity": "sha512-f6pDPw4QpjWnmVkYgOHjMXQXtGB4vbA45eZV9DjCF9OoCXsa+Pz32H2rLQRKbdpsfFllywOBI+GMGPYDJyrG/Q==" + }, + "@tp/tp-icon": { + "version": "1.0.0", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-icon/-/tp-icon-1.0.0.tgz", + "integrity": "sha512-/ETh6OPsDmU38niE68ngpZEMc/yaGhbvpuvZW67a1noQHiHOjW+kznhiqNYgV/x4AIxuslgvYquiGfWq+00a4Q==", + "requires": { + "@tp/tp-tooltip": "^1.0.0", + "lit": "^2.2.0" + }, + "dependencies": { + "lit": { + "version": "2.2.0", + "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.0.tgz", + "integrity": "sha512-FDyxUuczo6cJJY/2Bkgfh1872U4ikUvmK1Cb6+lYC1CW+QOo8CaWXCpvPKFzYsz0ojUxoruBLVrECc7VI2f1dQ==", + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + } + } + }, + "@tp/tp-input": { + "version": "1.0.3", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-input/-/tp-input-1.0.3.tgz", + "integrity": "sha512-PeygdnQt56pk89FJ6wU2iaKzxkNObrvDGHl/2WXPiQ67AX1kNFxBkJLgaGnTfTXqUYWxuGiY6fE8RilgkWZoyA==", + "requires": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + }, + "dependencies": { + "lit": { + "version": "2.2.0", + "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.0.tgz", + "integrity": "sha512-FDyxUuczo6cJJY/2Bkgfh1872U4ikUvmK1Cb6+lYC1CW+QOo8CaWXCpvPKFzYsz0ojUxoruBLVrECc7VI2f1dQ==", + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + } + } + }, + "@tp/tp-media-query": { + "version": "1.0.0", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-media-query/-/tp-media-query-1.0.0.tgz", + "integrity": "sha512-hxwkqgLDGXFmjQEMWxExJW/If1ppFerK9S5+I/P7qCKm3PMRDgafFvAPTpFLtqH3GYQ9z+uAAi2ZSNKqJ9Z7QQ==", + "requires": { + "lit": "^2.2.0" + }, + "dependencies": { + "lit": { + "version": "2.2.0", + "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.0.tgz", + "integrity": "sha512-FDyxUuczo6cJJY/2Bkgfh1872U4ikUvmK1Cb6+lYC1CW+QOo8CaWXCpvPKFzYsz0ojUxoruBLVrECc7VI2f1dQ==", + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + } + } + }, + "@tp/tp-tooltip": { + "version": "1.0.0", + "resolved": "https://verdaccio.codeblob.work/@tp%2ftp-tooltip/-/tp-tooltip-1.0.0.tgz", + "integrity": "sha512-UtrIK5KWcEiC+HnHOVbgg90j4RjHn3e9ehOBYPZsm6zO+tT7pQJJYFOtJqBW+DDV7jVfH3AvGKCxtzNiJXYvDw==", + "requires": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + }, + "dependencies": { + "lit": { + "version": "2.2.0", + "resolved": "https://verdaccio.codeblob.work/lit/-/lit-2.2.0.tgz", + "integrity": "sha512-FDyxUuczo6cJJY/2Bkgfh1872U4ikUvmK1Cb6+lYC1CW+QOo8CaWXCpvPKFzYsz0ojUxoruBLVrECc7VI2f1dQ==", + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + } + } + }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://verdaccio.codeblob.work/@types%2ftrusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" + }, + "lit-element": { + "version": "3.2.0", + "resolved": "https://verdaccio.codeblob.work/lit-element/-/lit-element-3.2.0.tgz", + "integrity": "sha512-HbE7yt2SnUtg5DCrWt028oaU4D5F4k/1cntAFHTkzY8ZIa8N0Wmu92PxSxucsQSOXlODFrICkQ5x/tEshKi13g==", + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "lit-html": { + "version": "2.2.0", + "resolved": "https://verdaccio.codeblob.work/lit-html/-/lit-html-2.2.0.tgz", + "integrity": "sha512-dJnevgV8VkCuOXLWrjQopDE8nSy8CzipZ/ATfYQv7z7Dct4abblcKecf50gkIScuwCTzKvRLgvTgV0zzagW4gA==", + "requires": { + "@types/trusted-types": "^2.0.2" + } + } + } +} diff --git a/package.json b/package.json index c39fdff..99dedcf 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,22 @@ { - "name": "@tp/tp-element", - "version": "0.0.1", + "name": "@tp/tp-dropdown", + "version": "1.0.0", "description": "", - "main": "tp-element.js", + "main": "tp-dropdown.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "https://gitea.codeblob.work/tp-elements/tp-element.git" + "url": "https://gitea.codeblob.work/tp-elements/tp-dropdown.git" }, "author": "trading_peter", "license": "Apache-2.0", "dependencies": { + "@tp/helpers": "^1.0.1", + "@tp/tp-icon": "^1.0.0", + "@tp/tp-input": "^1.0.3", + "@tp/tp-media-query": "^1.0.0", "lit": "^2.2.0" } } diff --git a/tp-dropdown.js b/tp-dropdown.js new file mode 100644 index 0000000..719577e --- /dev/null +++ b/tp-dropdown.js @@ -0,0 +1,823 @@ +/** +@license +Copyright (c) 2022 trading_peter +This program is available under Apache License Version 2.0 +*/ + +import '@tp/tp-icon/tp-icon.js'; +import '@tp/tp-input/tp-input.js'; +import '@tp/tp-media-query/tp-media-query.js'; +import { LitElement, html, svg, css } from 'lit'; +import { FormElement } from '@tp/helpers/form-element.js'; +import { Position } from '@tp/helpers/position.js'; +import { DomQuery } from '@tp/helpers/dom-query.js'; +import { EventHelpers } from '@tp/helpers/event-helpers.js'; +import { closest } from '@tp/helpers/closest.js'; +import { ControlState } from '@tp/helpers/control-state.js'; +import { isDefined } from '@tp/helpers/isDefined.js'; + +const mixins = [ + FormElement, + Position, + DomQuery, + EventHelpers, + ControlState +]; + +const BaseElement = mixins.reduce((baseClass, mixin) => { + return mixin(baseClass); +}, LitElement); + +class TpDropdown extends BaseElement { + static get styles() { + return [ + css` + :host { + display: block; + position: relative; + cursor: pointer; + outline: none; + + --shadow-2dp: 0 2px 2px 0 rgba(0, 0, 0, 0.14), + 0 1px 5px 0 rgba(0, 0, 0, 0.12), + 0 3px 1px -2px rgba(0, 0, 0, 0.2); + } + + [hidden] { + display: none; + } + + .selector { + display: flex; + flex-direction: row; + align-items: center; + border: solid 1px #9E9E9E; + border-radius: 2px; + } + + :host([invalid]) .selector { + border: solid 1px #B71C1C; + } + + :host([focused]) .selector { + border: solid 1px #039BE5; + } + + :host([no-overflow]) #selItem { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .selector #selItem { + flex: 1; + padding-left: 5px; + overflow: hidden; + text-overflow: ellipsis; + } + + #filter { + background: #f7f7f7; + padding: 7px 10px; + border-bottom: solid 1px #ececec; + } + + #filter tp-icon { + --tp-icon-width: 16px; + --tp-icon-height: 16px; + opacity: 0.5; + transition: opacity 0.3s; + } + + #filter tp-icon:hover { + opacity: 1; + } + + .toggle-icon-wrap { + /* Prevents the toggle icon from overflowing out of the control while it's rotated. */ + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + } + + #toggleIcon { + transition: opacity 0.3s, transform 0.2s; + transform-origin: center center; + transform: rotate(0deg); + opacity: 0.5; + } + + #toggleIcon[open] { + transform: rotate(180deg) !important; + opacity: 1; + } + + :host(:hover) #toggleIcon { + opacity: 1; + } + + #list { + pointer-events: none; + transition: opacity 0ms; + opacity: 0; + position: fixed; + z-index: 1; + height: auto; + } + + #list[open] { + pointer-events: all; + transition: opacity 180ms; + opacity: 1; + border-radius: 2px; + background: #FAFAFA; + height: auto; + box-shadow: var(--shadow-2dp); + } + + #itemList { + display: block; + overflow-y: auto; + } + + div[role="option"] { + padding: 5px 10px; + overflow: hidden; + text-overflow: ellipsis; + } + + div[role="option"]:hover { + background: #E0F7FA; + } + + div[role="option"]:focus { + background: #EEEEEE; + outline: none; + } + + div[role="option"][selected] { + background: #4FC3F7; + color: #FFFFFF; + } + + div[role="option"]:first-of-type { + margin-top: 10px; + } + + div[role="option"]:last-of-type { + margin-bottom: 10px; + } + + .add-item-label { + padding: 10px; + background: #FFFFFF; + color: #616161; + } + + .add-item-label:hover { + background: #039BE5; + } + + .error-message { + position: absolute; + bottom: 2px; + left: 5px; + right: 0; + font-size: 10px; + color: #B71C1C; + transition: opacity 0.3s; + opacity: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + } + + :host([invalid]) .error-message { + opacity: 1; + } + + @media all and (min-width: 0) and (max-width: 480px) { + + :host(:not([not-responsive])) #list { + top: 40px !important; + bottom: 40px !important; + left: 40px !important; + right: 40px !important; + max-width: none !important; + min-width: auto !important; + padding-bottom: 10px; + display: flex; + flex-direction: column; + } + + :host(:not([not-responsive])) #itemList { + max-height: none !important; + display: flex; + } + + :host(:not([not-responsive])) #filter { + font-size: 16px; + padding: 12px 10px; + } + + :host(:not([not-responsive])) div[role="option"] { + padding: 10px 10px; + } + + :host(:not([not-responsive])) div[role="option"]:last-of-type { + margin-bottom: 0; + } + } + ` + ]; + } + + render() { + const { label, isOpen, errorMessage, filterPlaceholder, extensible, filterable, items} = this; + + return html` + + +
+
+
${label}
+
${errorMessage}
+
+ +
+
+
+ this._filterTerm = e.target.value} .inert=${!isOpen} ?hidden=${!(filterable || extensible)}> + + + +
+ ${items.map(item => this._filter(item) ? html` +
${item.label}
+ ` : null)} +
+
+
+ `; + } + + static get properties() { + return { + items: { type: Array }, + isOpen: { type: Boolean }, + responsive: { type: Boolean }, + value: { type: String }, + default: { type: String }, + label: { type: String }, + errorMessage: { type: String }, + filterable: { type: Boolean }, + filterPlaceholder: { type: String }, + // Allow to add unknown items to the list (New items aren't added to the items array by the component. Listen for the `add-item` event to do that). + extensible: { type: Boolean }, + // If true, the dropdown fires the `add-item` event also on blur. Works only if `extensible` is set. + autoExtend: { type: Boolean, }, + focused: { type: Boolean, reflect: true, }, + readonly: { type: Boolean, reflect: true }, + /* + * Stores the first known value of the `value` property that is not falsy. + * If `items` changes and no longer holds the originally selected item, + * the dropdown will clear the selection. Nevertheless `_memorizedValue` + * still holds the old value of `value`. If `items` brings back the originally + * selected entry, the old `value` will be restored with `_memorizedValue` + * and the item will be selected again. + * + * Dropdown basically restores it's selection state if the right item comes + * back again. + */ + _memorizedValue: { type: String }, + _filterTerm: { type: String } + }; + } + + static get selectorIcon() { + return svg``; + } + + static get filterIcon() { + return svg` + + + `; + } + + static get addIcon() { + return svg` + + + `; + } + + constructor() { + super(); + this.items = []; + this._onDocClickHandler = () => this.close(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this._boundSetListPosition) { + window.removeEventListener('resize', this._boundSetListPosition); + document.removeEventListener('scroll', this._boundSetListPosition); + } + document.removeEventListener('click', this._onDocClickHandler, true); + this.unlisten(this, 'keydown', '_onKeyDown'); + this.unlisten(this.$.filter, 'keydown', '_onKeyDownFilter'); + this.unlisten(this.$.filterIcon, 'click', '_filterIconClicked'); + } + + firstUpdated() { + super.firstUpdated(); + this.listen(this, 'click', '_onClick'); + } + + updated(changes) { + super.updated(changes); + + if (changes.has('default')) { + this._setDefault(); + } + + if (changes.has('focused')) { + this._focusChanged(this.focused, changes.get('focused')); + } + + if (changes.has('value')) { + this._valueChanged(); + this.dispatchEvent(new CustomEvent('value-changed', { detail: this.value, bubbles: true, composed: true })); + } + } + + _queryUpdated(e) { + this.responsive = e.detail; + } + + _focusChanged(newState, oldState) { + if (newState) { + this.listen(this, 'keydown', '_onKeyDown'); + this.listen(this.$.filter, 'keydown', '_onKeyDownFilter'); + this.listen(this.$.filterIcon, 'click', '_filterIconClicked'); + + if (this.filterable) { + this.$.filter.focus(); + } + } else if (oldState) { + this.unlisten(this, 'keydown', '_onKeyDown'); + this.unlisten(this.$.filter, 'keydown', '_onKeyDownFilter'); + this.unlisten(this.$.filterIcon, 'click', '_filterIconClicked'); + + if (!newState) { + if (oldState && this.$.filter.value !== '') { + if (this.extensible && this.autoExtend) { + this._addItem(); + } else if (!this._selectByLabel(this.$.filter.value)) { + if (!this.value) { + this.$.filter.value = ''; + } + } + } + } + } + } + + _setDefault() { + if ((this.value === null || this.value === undefined) && this._hasItem(this.default)) { + this.value = this.default; + return true; + } + + return false; + } + + _hasItem(value) { + if (value === undefined) return false; + return Array.isArray(this.items) && this.items.find(item => item.value.toString() === value.toString()) !== undefined; + } + + _valueChanged() { + if (this._setDefault()) { + return; + } + + // Show selection in selector. + const idx = this.items.findIndex(item => item.value === this.value); + if (idx > -1) { + this.label = this.items[idx].label; + } + + this.invalid = false; + + setTimeout(() => { + var ariaSel = this.shadowRoot.querySelector('div[aria-selected]'); + var item = this.shadowRoot.querySelector('div[selected]'); + if (ariaSel) { + ariaSel.removeAttribute('aria-selected'); + } + if (item) { + item.setAttribute('aria-selected', true); + } + }, 0); + } + + _checkValue(newValue, oldValue) { + if (newValue === null || newValue === undefined) { + this.$.itemList.selection = null; + this.label = ''; + } + } + + /** + * Opens the menu. + */ + open() { + if (this.readonly) { + return; + } + + if (this._responsive) { + window.history.pushState({ is: 'tp-dropdown' }, null); + this.listen(window, 'popstate', 'close'); + } + + // If no items, we only open the list if the control is extensible. + if (this.items.length === 0) { + if (this.extensible || this.filterable) { + this.$.filter.focus(); + } else { + return; + } + } + + if (!this._boundSetListPosition) { + this._boundSetListPosition = this._setListPosition.bind(this); + } + + // Re-calc list position if window size is changed or document is scrolled. + // This is very important for smartphones with onscreen keyboards popping up. + window.addEventListener('resize', this._boundSetListPosition); + document.addEventListener('scroll', this._boundSetListPosition, { passive: true }); + document.addEventListener('click', this._onDocClickHandler, true); + this.listen(this.$.filter, 'click', '_filterClicked'); + + this._setListPosition(); + + this.isOpen = true; + this.$.selItem.focus(); + } + + _filterClicked(e) { + this._dontClose = true; + } + + /** + * Closes the menu. + */ + close() { + if (this.isOpen) { + this._closeJob = setTimeout(() => { + this._close(); + if (window.history.state && window.history.state.is === 'tp-dropdown') { + window.history.back(); + } + }, 50); + } + } + + _close() { + var idx = this.items.findIndex(item => item.value === this.value); + + if (idx === -1) { + this.label = ''; + } else { + this.label = this.items[idx].label; + } + + this.isOpen = false; + window.removeEventListener('resize', this._boundSetListPosition); + document.removeEventListener('scroll', this._boundSetListPosition); + document.removeEventListener('click', this._onDocClickHandler, true); + this.unlisten(this.$.filter, 'click', '_filterClicked'); + + this._filterTerm = ''; + } + + /** + * Toggles the menu. + */ + toggle() { + if (this.isOpen) { + this._close(); + } else { + this.open(); + } + } + + _setListPosition() { + var rect = this.getBoundingClientRect(); + var filterHeight = this.filterable ? this.$.filter.getBoundingClientRect().height : 0; + var winHeight = window.innerHeight; + + var spaceBottom = winHeight - rect.top - rect.height - filterHeight; + this.$.itemList.style.maxHeight = (spaceBottom - 20) + 'px'; + this.$.list.style.maxWidth = rect.width + 'px'; + this.$.list.style.minWidth = rect.width + 'px'; + + var useTopLayout = spaceBottom < 150; + + if (useTopLayout) { + this.$.itemList.style.maxHeight = (rect.top - 20) + 'px'; + } + + this._posFixed(this, this.$.list, { + spacing: 0, + valign: useTopLayout ? 'top' : 'bottom', + halign: 'left' + }); + + // Make list only take as much height as possible in responsive mode. + setTimeout(() => { + var lastItem = this.$.itemList.querySelector('div[role="option"]:last-of-type'); + if (lastItem) { + this.$.list.style.maxHeight = lastItem.getBoundingClientRect().top + 'px'; + } + }); + } + + /** + * Validate the control. + * + * @return {Boolean} False if invalid, else true. + */ + validate() { + if (!isDefined(this.value) && this.required) { + this.invalid = true; + return false; + } + + this.invalid = false; + return true; + } + + /** + * Reset the control if a parent era-form is reset. + */ + reset() { + this.invalid = false; + this._memorizedValue = undefined; + this._filterTerm = ''; + if (this.default !== undefined) { + this.value = this.default; + } else { + this.value = undefined; + } + } + + _itemsChanged() { + // In case `items` was set to null or undefined. + if (!this.items) { + this.items = []; + this.value = null; + return; + } + + const memValueDefined = this._memorizedValue !== null && this._memorizedValue !== undefined; + const foundValue = this.items.findIndex(item => item.value === this.value) > -1; + const foundMemVal = memValueDefined ? this.items.findIndex(item => item.value === this._memorizedValue) > -1 : false; + + if (!foundValue && foundMemVal) { + this.value = this._memorizedValue; + } + + if (!memValueDefined) { + this._memorizedValue = this.value; + } + + if (!foundValue && !foundMemVal) { + this.value = null; + } + + this._setDefault(); + + // Reset filter. + this._filterTerm = ''; + } + + _filter(item) { + if (!this._filterTerm) { + return true; + } + return new RegExp('.*' + this._filterTerm.toLowerCase() + '.*').test(item.label.toLowerCase()); + } + + _filterIconClicked() { + if (this.extensible) { + this._addItem(); + } + this._filterTerm = ''; + this.$.filter.focus(); + } + + _onClick(e) { + if (this._dontClose) { + setTimeout(() => { + clearTimeout(this._closeJob); + }, 20); + this._dontClose = false; + return; + } + + e.preventDefault(); + const itemEl = closest(e.composedPath()[0], 'div[role="option"]'); + if (itemEl === undefined) { + // We didn't click a item, so cancel the global close trigger. + setTimeout(() => { + clearTimeout(this._closeJob); + }, 20); + } else { + this.value = itemEl.value; + } + + var rootTarget = e.composedPath()[0]; + + // Toggle list only if it's not open or when the toggle icon is clicked or the click wasn't on the selector input. + if (!this.isOpen || closest(rootTarget, '#toggleIcon', true) !== undefined || (!closest(rootTarget, '#filter', true))) { + this.toggle(); + this._setListFocus(); + } + } + + _onKeyDown(e) { + if (this.readonly) { + return; + } + + if (this._isEsc(e)) { + if (this.isOpen) { + e.preventDefault(); + this.close(); + this.focus(); + } + return; + } + + if (this._isEnter(e)) { + if (this.isOpen) { + if (this.extensible && !this.$.itemList.querySelector('div[role="option"]:focus')) { + this._addItem(); + } + + this._selectItem(); + } + + this.toggle(); + + if (this.isOpen && this.filterable) { + this.$.filter.focus(); + } + } + + if (this.isOpen) { + if (this._isDownKey(e)) { + this._focusNextItem(); + e.preventDefault(); + } + + if (this._isUpKey(e)) { + this._focusPrevItem(); + e.preventDefault(); + } + } else { + if (this._isDownKey(e)) { + if (this.value === undefined && this.items && this.items.length > 0) { + this.$.itemList.select(0); + } else { + this.$.itemList.selectNext(); + } + e.preventDefault(); + } + + if (this._isUpKey(e)) { + if (this.value === undefined && this.items && this.items.length > 0) { + this.$.itemList.select(this.items.length - 1); + } else { + this.$.itemList.selectPrevious(); + } + e.preventDefault(); + } + } + } + + _isUpKey(e) { + return e.keyCode === 38; + } + + _isDownKey(e) { + return e.keyCode === 40; + } + + _isEsc(e) { + return e.keyCode === 27; + } + + _isEnter(e) { + return e.keyCode === 13; + } + + _onKeyDownFilter(e) { + if (this._isEsc(e)) { //esc + if (this._filterTerm) { + e.stopPropagation(); + } + + this._filterTerm = ''; + } + + if (this._isUpKey(e) || this._isDownKey(e)) { + this._setListFocus(); + e.preventDefault(); + e.stopPropagation(); + } + } + + // Add a new item to the list if the element is extensible. + _addItem() { + var label = this.$.filter.value; + if (label !== '') { + if (!this._selectByLabel(label)) { + this.fire('add-item', { label: label }); + } + } + } + + _selectByLabel(label) { + label = String(label).toLowerCase(); + if (Array.isArray(this.items)) { + for (var i = 0, li = this.items.length; i < li; ++i) { + if (String(this.items[i].label).toLowerCase() === String(label)) { + this.value = this.items[i].id; + return true; + } + } + } + + return false; + } + + _selectItem() { + var item = this.$.itemList.querySelector('div[role="option"]:focus'); + + if (item) { + this.value = item.value; + } + } + + _setListFocus() { + var item = this.$.itemList.querySelector('div[role="option"].iron-selected') || this.$.itemList.querySelector('div[role="option"]'); + + if (item) { + item.focus(); + } + } + + _focusNextItem() { + var item = this.$.itemList.querySelector('div[role="option"]:focus'); + if (!item) { + return; + } + + var next = item.nextElementSibling; + + if (!next || next.tagName !== 'DIV') { + next = this.$.itemList.querySelector('div[role="option"]'); + } + + if (next) { + next.focus(); + } + } + + _focusPrevItem() { + var item = this.$.itemList.querySelector('div[role="option"]:focus'); + if (!item) { + return; + } + + var next = item.previousElementSibling; + + if (!next || next.tagName !== 'DIV') { + next = this.$.itemList.querySelector('div[role="option"]:last-of-type'); + } + + if (next) { + next.focus(); + } + } +} + +window.customElements.define('tp-dropdown', TpDropdown); diff --git a/tp-element.js b/tp-element.js deleted file mode 100644 index 6a92a2f..0000000 --- a/tp-element.js +++ /dev/null @@ -1,35 +0,0 @@ -/** -@license -Copyright (c) 2022 trading_peter -This program is available under Apache License Version 2.0 -*/ - -import { LitElement, html, css } from 'lit'; - -class TpElement extends LitElement { - static get styles() { - return [ - css` - :host { - display: block; - } - ` - ]; - } - - render() { - const { } = this; - - return html` - - `; - } - - static get properties() { - return { }; - } - - -} - -window.customElements.define('tp-element', TpElement);