Merge changes from topic "gr-dropdown-to-ts" * changes: Move gr-dropdown to typescript Rename files to preserve history
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js deleted file mode 100644 index 0f3d566..0000000 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js +++ /dev/null
@@ -1,329 +0,0 @@ -/** - * @license - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import '@polymer/iron-dropdown/iron-dropdown.js'; -import '../gr-button/gr-button.js'; -import '../gr-cursor-manager/gr-cursor-manager.js'; -import '../gr-rest-api-interface/gr-rest-api-interface.js'; -import '../gr-tooltip-content/gr-tooltip-content.js'; -import '../../../styles/shared-styles.js'; -import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; -import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; -import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; -import {PolymerElement} from '@polymer/polymer/polymer-element.js'; -import {htmlTemplate} from './gr-dropdown_html.js'; -import {getBaseUrl} from '../../../utils/url-util.js'; -import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js'; - -const REL_NOOPENER = 'noopener'; -const REL_EXTERNAL = 'external'; - -/** - * @extends PolymerElement - */ -class GrDropdown extends KeyboardShortcutMixin(GestureEventListeners( - LegacyElementMixin(PolymerElement))) { - static get template() { return htmlTemplate; } - - static get is() { return 'gr-dropdown'; } - /** - * Fired when a non-link dropdown item with the given ID is tapped. - * - * @event tap-item-<id> - */ - - /** - * Fired when a non-link dropdown item is tapped. - * - * @event tap-item - */ - - static get properties() { - return { - items: { - type: Array, - observer: '_resetCursorStops', - }, - downArrow: Boolean, - topContent: Object, - horizontalAlign: { - type: String, - value: 'left', - }, - - /** - * Style the dropdown trigger as a link (rather than a button). - */ - link: { - type: Boolean, - value: false, - }, - - verticalOffset: { - type: Number, - value: 40, - }, - - /** - * List the IDs of dropdown buttons to be disabled. (Note this only - * diisables bittons and not link entries.) - */ - disabledIds: { - type: Array, - value() { return []; }, - }, - - /** - * The elements of the list. - */ - _listElements: { - type: Array, - value() { return []; }, - }, - }; - } - - get keyBindings() { - return { - 'down': '_handleDown', - 'enter space': '_handleEnter', - 'tab': '_handleTab', - 'up': '_handleUp', - }; - } - - /** - * Handle the up key. - * - * @param {!Event} e - */ - _handleUp(e) { - if (this.$.dropdown.opened) { - e.preventDefault(); - e.stopPropagation(); - this.$.cursor.previous(); - } else { - this._open(); - } - } - - /** - * Handle the down key. - * - * @param {!Event} e - */ - _handleDown(e) { - if (this.$.dropdown.opened) { - e.preventDefault(); - e.stopPropagation(); - this.$.cursor.next(); - } else { - this._open(); - } - } - - /** - * Handle the tab key. - * - * @param {!Event} e - */ - _handleTab(e) { - if (this.$.dropdown.opened) { - // Tab in a native select is a no-op. Emulate this. - e.preventDefault(); - e.stopPropagation(); - } - } - - /** - * Handle the enter key. - * - * @param {!Event} e - */ - _handleEnter(e) { - e.preventDefault(); - e.stopPropagation(); - if (this.$.dropdown.opened) { - // TODO(milutin): This solution is not particularly robust in general. - // Since gr-tooltip-content click on shadow dom is not propagated down, - // we have to target `a` inside it. - const el = this.$.cursor.target.querySelector(':not([hidden]) a'); - if (el) { el.click(); } - } else { - this._open(); - } - } - - /** - * Handle a click on the iron-dropdown element. - * - * @param {!Event} e - */ - _handleDropdownClick(e) { - this._close(); - } - - /** - * Handle a click on the button to open the dropdown. - * - * @param {!Event} e - */ - _dropdownTriggerTapHandler(e) { - e.preventDefault(); - e.stopPropagation(); - if (this.$.dropdown.opened) { - this._close(); - } else { - this._open(); - } - } - - /** - * Open the dropdown and initialize the cursor. - */ - _open() { - this.$.dropdown.open(); - this._resetCursorStops(); - this.$.cursor.setCursorAtIndex(0); - this.$.cursor.target.focus(); - } - - _close() { - // async is needed so that that the click event is fired before the - // dropdown closes (This was a bug for touch devices). - this.async(() => { - this.$.dropdown.close(); - }, 1); - } - - /** - * Get the class for a top-content item based on the given boolean. - * - * @param {boolean} bold Whether the item is bold. - * @return {string} The class for the top-content item. - */ - _getClassIfBold(bold) { - return bold ? 'bold-text' : ''; - } - - /** - * Build a URL for the given host and path. The base URL will be only added, - * if it is not already included in the path. - * - * @param {!string} host - * @param {!string} path - * @return {!string} The scheme-relative URL. - */ - _computeURLHelper(host, path) { - const base = path.startsWith(getBaseUrl()) ? - '' : getBaseUrl(); - return '//' + host + base + path; - } - - /** - * Build a scheme-relative URL for the current host. Will include the base - * URL if one is present. Note: the URL will be scheme-relative but absolute - * with regard to the host. - * - * @param {!string} path The path for the URL. - * @return {!string} The scheme-relative URL. - */ - _computeRelativeURL(path) { - const host = window.location.host; - return this._computeURLHelper(host, path); - } - - /** - * Compute the URL for a link object. - * - * @param {!Object} link The object describing the link. - * @return {!string} The URL. - */ - _computeLinkURL(link) { - if (typeof link.url === 'undefined') { - return ''; - } - if (link.target || !link.url.startsWith('/')) { - return link.url; - } - return this._computeRelativeURL(link.url); - } - - /** - * Compute the value for the rel attribute of an anchor for the given link - * object. If the link has a target value, then the rel must be "noopener" - * for security reasons. - * - * @param {!Object} link The object describing the link. - * @return {?string} The rel value for the link. - */ - _computeLinkRel(link) { - // Note: noopener takes precedence over external. - if (link.target) { return REL_NOOPENER; } - if (link.external) { return REL_EXTERNAL; } - return null; - } - - /** - * Handle a click on an item of the dropdown. - * - * @param {!Event} e - */ - _handleItemTap(e) { - const id = e.target.getAttribute('data-id'); - const item = this.items.find(item => item.id === id); - if (id && !this.disabledIds.includes(id)) { - if (item) { - this.dispatchEvent(new CustomEvent('tap-item', - {detail: item, bubbles: true, composed: true})); - } - this.dispatchEvent(new CustomEvent('tap-item-' + id)); - } - } - - /** - * If a dropdown item is shown as a button, get the class for the button. - * - * @param {string} id - * @param {!Object} disabledIdsRecord The change record for the disabled IDs - * list. - * @return {!string} The class for the item button. - */ - _computeDisabledClass(id, disabledIdsRecord) { - return disabledIdsRecord.base.includes(id) ? 'disabled' : ''; - } - - /** - * Recompute the stops for the dropdown item cursor. - */ - _resetCursorStops() { - if (this.items && this.items.length > 0 && this.$.dropdown.opened) { - flush(); - this._listElements = Array.from( - dom(this.root).querySelectorAll('li')); - } - } - - _computeHasTooltip(tooltip) { - return !!tooltip; - } - - _computeIsDownload(link) { - return !!link.download; - } -} - -customElements.define(GrDropdown.is, GrDropdown);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts new file mode 100644 index 0000000..2c9b4b0 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.ts
@@ -0,0 +1,344 @@ +/** + * @license + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@polymer/iron-dropdown/iron-dropdown'; +import '../gr-button/gr-button'; +import '../gr-cursor-manager/gr-cursor-manager'; +import '../gr-rest-api-interface/gr-rest-api-interface'; +import '../gr-tooltip-content/gr-tooltip-content'; +import '../../../styles/shared-styles'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin'; +import {PolymerElement} from '@polymer/polymer/polymer-element'; +import {htmlTemplate} from './gr-dropdown_html'; +import {getBaseUrl} from '../../../utils/url-util'; +import {KeyboardShortcutMixin} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin'; +import {IronDropdownElement} from '@polymer/iron-dropdown/iron-dropdown'; +import {GrCursorManager} from '../gr-cursor-manager/gr-cursor-manager'; +import {property, customElement, observe} from '@polymer/decorators'; + +const REL_NOOPENER = 'noopener'; +const REL_EXTERNAL = 'external'; + +declare global { + interface HTMLElementTagNameMap { + 'gr-dropdown': GrDropdown; + } +} + +export interface GrDropdown { + $: { + dropdown: IronDropdownElement; + cursor: GrCursorManager; + }; +} + +interface DropdownLink { + url?: string; + name?: string; + external?: boolean; + target?: string; + download?: boolean; + id?: string; + tooltip?: string; +} + +interface DisableIdsRecord { + base: string[]; +} + +interface Content { + text: string; + bold?: boolean; +} + +@customElement('gr-dropdown') +export class GrDropdown extends KeyboardShortcutMixin( + GestureEventListeners(LegacyElementMixin(PolymerElement)) +) { + static get template() { + return htmlTemplate; + } + + /** + * Fired when a non-link dropdown item with the given ID is tapped. + * + * @event tap-item-<id> + */ + + /** + * Fired when a non-link dropdown item is tapped. + * + * @event tap-item + */ + + @property({type: Array}) + items?: DropdownLink[]; + + @property({type: Boolean}) + downArrow?: boolean; + + @property({type: Array}) + topContent?: Content[]; + + @property({type: String}) + horizontalAlign = 'left'; + + /** + * Style the dropdown trigger as a link (rather than a button). + */ + + link = false; + + @property({type: Number}) + verticalOffset = 40; + + /** + * List the IDs of dropdown buttons to be disabled. (Note this only + * disables buttons and not link entries.) + */ + @property({type: Array}) + disabledIds: string[] = []; + + /** + * The elements of the list. + */ + @property({type: Array}) + _listElements: Element[] = []; + + get keyBindings() { + return { + down: '_handleDown', + 'enter space': '_handleEnter', + tab: '_handleTab', + up: '_handleUp', + }; + } + + /** + * Handle the up key. + */ + _handleUp(e: MouseEvent) { + if (this.$.dropdown.opened) { + e.preventDefault(); + e.stopPropagation(); + this.$.cursor.previous(); + } else { + this._open(); + } + } + + /** + * Handle the down key. + */ + _handleDown(e: MouseEvent) { + if (this.$.dropdown.opened) { + e.preventDefault(); + e.stopPropagation(); + this.$.cursor.next(); + } else { + this._open(); + } + } + + /** + * Handle the tab key. + */ + _handleTab(e: MouseEvent) { + if (this.$.dropdown.opened) { + // Tab in a native select is a no-op. Emulate this. + e.preventDefault(); + e.stopPropagation(); + } + } + + /** + * Handle the enter key. + */ + _handleEnter(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + if (this.$.dropdown.opened) { + // TODO(milutin): This solution is not particularly robust in general. + // Since gr-tooltip-content click on shadow dom is not propagated down, + // we have to target `a` inside it. + if (this.$.cursor.target !== null) { + const el = this.$.cursor.target.querySelector(':not([hidden]) a'); + if (el) { + (el as HTMLElement).click(); + } + } + } else { + this._open(); + } + } + + /** + * Handle a click on the iron-dropdown element. + */ + _handleDropdownClick() { + this._close(); + } + + /** + * Handle a click on the button to open the dropdown. + */ + _dropdownTriggerTapHandler(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + if (this.$.dropdown.opened) { + this._close(); + } else { + this._open(); + } + } + + /** + * Open the dropdown and initialize the cursor. + */ + _open() { + this.$.dropdown.open(); + this._resetCursorStops(); + this.$.cursor.setCursorAtIndex(0); + if (this.$.cursor.target !== null) this.$.cursor.target.focus(); + } + + _close() { + // async is needed so that that the click event is fired before the + // dropdown closes (This was a bug for touch devices). + this.async(() => { + this.$.dropdown.close(); + }, 1); + } + + /** + * Get the class for a top-content item based on the given boolean. + * + * @param bold Whether the item is bold. + * @return The class for the top-content item. + */ + _getClassIfBold(bold: boolean) { + return bold ? 'bold-text' : ''; + } + + /** + * Build a URL for the given host and path. The base URL will be only added, + * if it is not already included in the path. + * + * @return The scheme-relative URL. + */ + _computeURLHelper(host: string, path: string) { + const base = path.startsWith(getBaseUrl()) ? '' : getBaseUrl(); + return '//' + host + base + path; + } + + /** + * Build a scheme-relative URL for the current host. Will include the base + * URL if one is present. Note: the URL will be scheme-relative but absolute + * with regard to the host. + * + * @param path The path for the URL. + * @return The scheme-relative URL. + */ + _computeRelativeURL(path: string) { + const host = window.location.host; + return this._computeURLHelper(host, path); + } + + /** + * Compute the URL for a link object. + */ + _computeLinkURL(link: DropdownLink) { + if (typeof link.url === 'undefined') { + return ''; + } + if (link.target || !link.url.startsWith('/')) { + return link.url; + } + return this._computeRelativeURL(link.url); + } + + /** + * Compute the value for the rel attribute of an anchor for the given link + * object. If the link has a target value, then the rel must be "noopener" + * for security reasons. + */ + _computeLinkRel(link: DropdownLink) { + // Note: noopener takes precedence over external. + if (link.target) { + return REL_NOOPENER; + } + if (link.external) { + return REL_EXTERNAL; + } + return null; + } + + /** + * Handle a click on an item of the dropdown. + */ + _handleItemTap(e: MouseEvent) { + if (e.target === null || !this.items) { + return; + } + const id = (e.target as Element).getAttribute('data-id'); + const item = this.items.find(item => item.id === id); + if (id && !this.disabledIds.includes(id)) { + if (item) { + this.dispatchEvent( + new CustomEvent('tap-item', { + detail: item, + bubbles: true, + composed: true, + }) + ); + } + this.dispatchEvent(new CustomEvent('tap-item-' + id)); + } + } + + /** + * If a dropdown item is shown as a button, get the class for the button. + * + * @param disabledIdsRecord The change record for the disabled IDs + * list. + * @return The class for the item button. + */ + _computeDisabledClass(id: string, disabledIdsRecord: DisableIdsRecord) { + return disabledIdsRecord.base.includes(id) ? 'disabled' : ''; + } + + /** + * Recompute the stops for the dropdown item cursor. + */ + @observe('items') + _resetCursorStops() { + if (this.items && this.items.length > 0 && this.$.dropdown.opened) { + flush(); + this._listElements = + this.root !== null ? Array.from(this.root.querySelectorAll('li')) : []; + } + } + + _computeHasTooltip(tooltip?: string) { + return !!tooltip; + } + + _computeIsDownload(link: DropdownLink) { + return !!link.download; + } +}