Merge "Migrate gr-tooltip-content to Lit"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
index 03a1ef1..6e68ae7 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.ts
@@ -119,15 +119,7 @@
private renderOption(option: PluginOption) {
return html`
<section class="section ${option.info.type}">
- <span class="title">
- <gr-tooltip-content
- has-tooltip="${option.info.description}"
- show-icon="${option.info.description}"
- title="${option.info.description}"
- >
- <span>${option.info.display_name}</span>
- </gr-tooltip-content>
- </span>
+ <span class="title"> ${this.renderOptionTitle(option)} </span>
<span class="value">
${this.renderOptionDetail(option)} ${this.renderInherited(option)}
</span>
@@ -135,6 +127,18 @@
`;
}
+ private renderOptionTitle(option: PluginOption) {
+ const titleName = html`<span>${option.info.display_name}</span>`;
+ if (!option.info.description) return titleName;
+ return html` <gr-tooltip-content
+ has-tooltip
+ show-icon
+ title="${option.info.description}"
+ >
+ ${titleName}
+ </gr-tooltip-content>`;
+ }
+
private renderOptionDetail(option: PluginOption) {
if (option.info.type === ConfigParameterInfoType.ARRAY) {
return html`
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index f0d9268..f10ffd0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -281,7 +281,7 @@
class="cell size"
hidden$="[[_computeIsColumnHidden('Size', visibleChangeTableColumns)]]"
>
- <gr-tooltip-content has-tooltip="" title="[[_computeSizeTooltip(change)]]">
+ <gr-tooltip-content has-tooltip title="[[_computeSizeTooltip(change)]]">
<template is="dom-if" if="[[_changeSize]]">
<span>[[_changeSize]]</span>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
index c105aaa..a37daaa 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.ts
@@ -354,8 +354,8 @@
></gr-commit-info>
<gr-tooltip-content
id="parentNotCurrentMessage"
- has-tooltip=""
- show-icon=""
+ has-tooltip
+ show-icon
title$="[[_notCurrentMessage]]"
></gr-tooltip-content>
</li>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index b0a4454..d57aca8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -479,7 +479,7 @@
class="commentThreads"
>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"
>
<span>Comments</span></gr-tooltip-content
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
index 91f7e46..85f0330 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.ts
@@ -181,7 +181,7 @@
hidden=""
>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="Diff preferences"
>
<gr-button
@@ -197,7 +197,7 @@
</div>
<span class="downloadContainer desktop">
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG,
ShortcutSection.ACTIONS)]]"
>
@@ -214,7 +214,7 @@
if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"
>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
ShortcutSection.FILE_LIST)]]">
<gr-button
@@ -225,7 +225,7 @@
>Expand All</gr-button
>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="[[createTitle(Shortcut.TOGGLE_ALL_INLINE_DIFFS,
ShortcutSection.FILE_LIST)]]">
<gr-button
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index 08e5e3b..c90cfcc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -90,26 +90,26 @@
assert.isFalse(computeSpy.lastCall.returnValue);
});
- test('fileViewActions are properly hidden', () => {
+ test('fileViewActions are properly hidden', async () => {
const actions = element.shadowRoot
.querySelector('.fileViewActions');
assert.equal(getComputedStyle(actions).display, 'none');
element.filesExpanded = FilesExpandedState.SOME;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(actions).display, 'none');
element.filesExpanded = FilesExpandedState.ALL;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(actions).display, 'none');
element.filesExpanded = FilesExpandedState.NONE;
- flush();
+ await flush();
assert.equal(getComputedStyle(actions).display, 'none');
});
- test('expand/collapse buttons are toggled correctly', () => {
+ test('expand/collapse buttons are toggled correctly', async () => {
// Only the expand button should be visible in the initial state when
// NO files are expanded.
element.shownFileCount = 10;
- flush();
+ await flush();
const expandBtn = element.shadowRoot.querySelector('#expandBtn');
const collapseBtn = element.shadowRoot.querySelector('#collapseBtn');
assert.notEqual(getComputedStyle(expandBtn).display, 'none');
@@ -118,19 +118,19 @@
// Both expand and collapse buttons should be visible when SOME files are
// expanded.
element.filesExpanded = FilesExpandedState.SOME;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(expandBtn).display, 'none');
assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
// Only the collapse button should be visible when ALL files are expanded.
element.filesExpanded = FilesExpandedState.ALL;
- flush();
+ await flush();
assert.equal(getComputedStyle(expandBtn).display, 'none');
assert.notEqual(getComputedStyle(collapseBtn).display, 'none');
// Only the expand button should be visible when NO files are expanded.
element.filesExpanded = FilesExpandedState.NONE;
- flush();
+ await flush();
assert.notEqual(getComputedStyle(expandBtn).display, 'none');
assert.equal(getComputedStyle(collapseBtn).display, 'none');
});
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
index 618403c..421cd6e 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.ts
@@ -119,7 +119,7 @@
>
<template is="dom-repeat" items="[[_items]]" as="value">
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title$="[[_computeLabelValueTitle(labels, label.name, value)]]"
data-name$="[[label.name]]"
data-value$="[[value]]"
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
index 4e66b4e..34e959b 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.js
@@ -160,30 +160,6 @@
checkAriaCheckedValid();
});
- test('do not display tooltips on touch devices', async () => {
- const verifiedTooltip = element.shadowRoot
- .querySelector('iron-selector > gr-tooltip-content');
-
- // On touch devices, tooltips should not be shown.
- verifiedTooltip._isTouchDevice = true;
- await flush();
- verifiedTooltip._handleShowTooltip();
- await flush();
- assert.isNotOk(verifiedTooltip._tooltip);
- verifiedTooltip._handleHideTooltip();
- await flush();
- assert.isNotOk(verifiedTooltip._tooltip);
-
- // On other devices, tooltips should be shown.
- verifiedTooltip._isTouchDevice = false;
- verifiedTooltip._handleShowTooltip();
- await flush();
- assert.isOk(verifiedTooltip._tooltip);
- verifiedTooltip._handleHideTooltip();
- await flush();
- assert.isNotOk(verifiedTooltip._tooltip);
- });
-
test('_computeLabelValue', () => {
assert.strictEqual(element._computeLabelValue(element.labels,
element.permittedLabels,
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 3fedcd9..1973fe6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -442,7 +442,7 @@
</template>
</template>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="[[_computeAttentionButtonTitle(_sendDisabled)]]"
>
<gr-button
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index c197599..76fa353 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -605,12 +605,12 @@
title="${runButtonDisabled
? 'Disabled. Unselect checks without a "Run" action to enable the button.'
: ''}"
- has-tooltip="${runButtonDisabled}"
+ ?has-tooltip=${runButtonDisabled}
>
<gr-button
class="font-normal"
link
- ?disabled="${runButtonDisabled}"
+ ?disabled=${runButtonDisabled}
@click="${() => {
actions.forEach(action => this.checksService.triggerAction(action));
}}"
@@ -623,7 +623,7 @@
private renderCollapseButton() {
return html`
<gr-tooltip-content
- has-tooltip="true"
+ has-tooltip
title="${this.collapsed ? 'Expand runs panel' : 'Collapse runs panel'}"
>
<gr-button
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
index 2ca2744b..455bd4e 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.ts
@@ -80,8 +80,8 @@
}
</style>
<gr-tooltip-content
- has-tooltip=""
- position-below=""
+ has-tooltip
+ position-below
title="[[tooltipText]]"
max-width="40em"
>
@@ -101,9 +101,8 @@
</a>
</template>
<template is="dom-if" if="[[!hasStatusLink(revertedChange, resolveWeblinks, status)]]">
- <div class="chip" aria-label$="Label: [[status]]">
- [[_computeStatusString(status)]]
- </div>
+ <div class="chip" aria-label$="Label: [[status]]"
+ >[[_computeStatusString(status)]]</div>
</template>
</gr-tooltip-content>
</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
index 4cb7738..d1496fb 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.ts
@@ -275,10 +275,10 @@
</template>
<gr-tooltip-content
class="draftTooltip"
- has-tooltip=""
+ has-tooltip
title="[[_computeDraftTooltip(_unableToSave)]]"
max-width="20em"
- show-icon=""
+ show-icon
>
<span class="draftLabel">[[_computeDraftText(_unableToSave)]]</span>
</gr-tooltip-content>
@@ -357,7 +357,7 @@
<div class="respectfulReviewTip">
<div>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="Tips for respectful code reviews."
>
<iron-icon
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
index 99be86b..0cd522a 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.ts
@@ -110,7 +110,7 @@
type="text"
@click="${this._handleInputClick}"
readonly=""
- bind-value=${this.text}
+ bind-value=${this.text || ''}
>
<input
id="input"
@@ -119,7 +119,7 @@
type="text"
@click="${this._handleInputClick}"
readonly=""
- .value=${this.text}
+ .value=${this.text || ''}
part="text-container-style"
/>
</iron-input>
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
index 94c6754..552bd08 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.ts
@@ -102,7 +102,7 @@
<tr class="labelValueContainer">
<td>
<gr-tooltip-content
- has-tooltip=""
+ has-tooltip
title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]"
>
<gr-label class$="[[mappedLabel.className]] voteChip font-small">
@@ -117,7 +117,7 @@
></gr-account-link>
</td>
<td>
- <gr-tooltip-content has-tooltip="" title="Remove vote">
+ <gr-tooltip-content has-tooltip title="Remove vote">
<gr-button
link=""
aria-label="Remove vote"
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index 9d228ab..2b9a868 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -15,10 +15,13 @@
* limitations under the License.
*/
import '../gr-icons/gr-icons';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-tooltip-content_html';
-import {TooltipMixin} from '../../../mixins/gr-tooltip-mixin/gr-tooltip-mixin';
-import {customElement, property} from '@polymer/decorators';
+import '../gr-tooltip/gr-tooltip';
+import {getRootElement} from '../../../scripts/rootElement';
+import {GrTooltip} from '../gr-tooltip/gr-tooltip';
+import {css, html, LitElement, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
+
+const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
declare global {
interface HTMLElementTagNameMap {
@@ -26,21 +29,202 @@
}
}
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
-/**
- * Transclude anything inside and wrap them to support tooltip functionality.
- */
@customElement('gr-tooltip-content')
-export class GrTooltipContent extends base {
- static get template() {
- return htmlTemplate;
- }
+export class GrTooltipContent extends LitElement {
+ @property({type: Boolean, attribute: 'has-tooltip', reflect: true})
+ hasTooltip = false;
- @property({type: String, reflectToAttribute: true})
+ @property({type: Boolean, attribute: 'position-below', reflect: true})
+ positionBelow = false;
+
+ @property({type: String, attribute: 'max-width', reflect: true})
maxWidth?: string;
@property({type: Boolean})
showIcon = false;
+
+ // Should be private but used in tests.
+ @state()
+ isTouchDevice = 'ontouchstart' in document.documentElement;
+
+ // Should be private but used in tests.
+ tooltip: GrTooltip | null = null;
+
+ @state()
+ private originalTitle = '';
+
+ private hasSetupTooltipListeners = false;
+
+ private readonly windowScrollHandler: () => void;
+
+ private readonly showHandler: () => void;
+
+ private readonly hideHandler: (e: Event) => void;
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
+ constructor() {
+ super();
+ this.windowScrollHandler = () => this._handleWindowScroll();
+ this.showHandler = () => this._handleShowTooltip();
+ this.hideHandler = (e: Event | undefined) => this._handleHideTooltip(e);
+ }
+
+ override disconnectedCallback() {
+ this._handleHideTooltip(undefined);
+ this.removeEventListener('mouseenter', this.showHandler);
+ window.removeEventListener('scroll', this.windowScrollHandler);
+ super.disconnectedCallback();
+ }
+
+ static override get styles() {
+ return [
+ css`
+ iron-icon {
+ width: var(--line-height-normal);
+ height: var(--line-height-normal);
+ vertical-align: top;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <slot></slot>
+ ${this.renderIcon()}
+ `;
+ }
+
+ renderIcon() {
+ if (!this.showIcon) return;
+ return html`<iron-icon icon="gr-icons:info"></iron-icon>`;
+ }
+
+ override updated(changedProperties: PropertyValues) {
+ if (changedProperties.has('hasTooltip')) {
+ this.setupTooltipListeners();
+ }
+ }
+
+ private setupTooltipListeners() {
+ if (!this.hasTooltip) {
+ if (this.hasSetupTooltipListeners) {
+ // if attribute set to false, remove the listener
+ this.removeEventListener('mouseenter', this.showHandler);
+ this.hasSetupTooltipListeners = false;
+ }
+ return;
+ }
+
+ if (this.hasSetupTooltipListeners) {
+ return;
+ }
+ this.hasSetupTooltipListeners = true;
+ this.addEventListener('mouseenter', this.showHandler);
+ }
+
+ _handleShowTooltip() {
+ if (this.isTouchDevice) {
+ return;
+ }
+
+ if (
+ !this.hasAttribute('title') ||
+ this.getAttribute('title') === '' ||
+ this.tooltip
+ ) {
+ return;
+ }
+
+ // Store the title attribute text then set it to an empty string to
+ // prevent it from showing natively.
+ this.originalTitle = this.getAttribute('title') || '';
+ this.setAttribute('title', '');
+
+ const tooltip = document.createElement('gr-tooltip');
+ tooltip.text = this.originalTitle;
+ tooltip.maxWidth = this.getAttribute('max-width') || '';
+ tooltip.positionBelow = this.hasAttribute('position-below');
+
+ // Set visibility to hidden before appending to the DOM so that
+ // calculations can be made based on the element’s size.
+ tooltip.style.visibility = 'hidden';
+ getRootElement().appendChild(tooltip);
+ this._positionTooltip(tooltip);
+ tooltip.style.visibility = 'initial';
+
+ this.tooltip = tooltip;
+ window.addEventListener('scroll', this.windowScrollHandler);
+ this.addEventListener('mouseleave', this.hideHandler);
+ this.addEventListener('click', this.hideHandler);
+ tooltip.addEventListener('mouseleave', this.hideHandler);
+ }
+
+ _handleHideTooltip(e: Event | undefined) {
+ if (this.isTouchDevice) {
+ return;
+ }
+ if (!this.hasAttribute('title') || !this.originalTitle) {
+ return;
+ }
+ // Do not hide if mouse left this or this.tooltip and came to this or
+ // this.tooltip
+ if (
+ (e as MouseEvent)?.relatedTarget === this.tooltip ||
+ (e as MouseEvent)?.relatedTarget === this
+ ) {
+ return;
+ }
+
+ window.removeEventListener('scroll', this.windowScrollHandler);
+ this.removeEventListener('mouseleave', this.hideHandler);
+ this.removeEventListener('click', this.hideHandler);
+ this.setAttribute('title', this.originalTitle);
+ this.tooltip?.removeEventListener('mouseleave', this.hideHandler);
+
+ if (this.tooltip?.parentNode) {
+ this.tooltip.parentNode.removeChild(this.tooltip);
+ }
+ this.tooltip = null;
+ }
+
+ _handleWindowScroll() {
+ if (!this.tooltip) {
+ return;
+ }
+ // This wait is needed for tooltips to be positioned correctly in Firefox
+ // and Safari.
+ this.updateComplete.then(() => this._positionTooltip(this.tooltip));
+ }
+
+ // private but used in tests.
+ async _positionTooltip(tooltip: GrTooltip | null) {
+ if (tooltip === null) return;
+ const rect = this.getBoundingClientRect();
+ const boxRect = tooltip.getBoundingClientRect();
+ if (!tooltip.parentElement) {
+ return;
+ }
+ const parentRect = tooltip.parentElement.getBoundingClientRect();
+ const top = rect.top - parentRect.top;
+ const left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
+ const right = parentRect.width - left - boxRect.width;
+ if (left < 0) {
+ tooltip.updateStyles({
+ '--gr-tooltip-arrow-center-offset': `${left}px`,
+ });
+ } else if (right < 0) {
+ tooltip.updateStyles({
+ '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
+ });
+ }
+ tooltip.style.left = `${Math.max(0, left)}px`;
+
+ if (!this.positionBelow) {
+ tooltip.style.top = `${Math.max(0, top)}px`;
+ tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
+ } else {
+ tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
+ }
+ }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
deleted file mode 100644
index 952420d..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style>
- iron-icon {
- width: var(--line-height-normal);
- height: var(--line-height-normal);
- vertical-align: top;
- }
- </style>
- <slot></slot
- ><!--
- --><iron-icon icon="gr-icons:info" hidden$="[[!showIcon]]"></iron-icon>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
index f905eaa..8d3bbb0 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
@@ -17,35 +17,162 @@
import '../../../test/common-test-setup-karma.js';
import './gr-tooltip-content.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-const basicFixture = fixtureFromTemplate(html`
-<gr-tooltip-content>
- </gr-tooltip-content>
-`);
+const basicFixture = fixtureFromElement('gr-tooltip-content');
suite('gr-tooltip-content tests', () => {
let element;
- setup(() => {
+
+ function makeTooltip(tooltipRect, parentRect) {
+ return {
+ getBoundingClientRect() { return tooltipRect; },
+ updateStyles: sinon.stub(),
+ style: {left: 0, top: 0},
+ parentElement: {
+ getBoundingClientRect() { return parentRect; },
+ },
+ };
+ }
+
+ setup(async () => {
element = basicFixture.instantiate();
+ element.title = 'title';
+ await element.updateComplete;
});
test('icon is not visible by default', () => {
- assert.equal(dom(element.root)
- .querySelector('iron-icon').hidden, true);
+ assert.isNotOk(element.shadowRoot.querySelector('iron-icon'));
});
- test('position-below attribute is reflected', () => {
+ test('icon is visible with showIcon property', async () => {
+ element.showIcon = true;
+ await element.updateComplete;
+ assert.isOk(element.shadowRoot.querySelector('iron-icon'));
+ });
+
+ test('position-below attribute is reflected', async () => {
assert.isFalse(element.hasAttribute('position-below'));
element.positionBelow = true;
+ await element.updateComplete;
assert.isTrue(element.hasAttribute('position-below'));
});
- test('icon is visible with showIcon property', () => {
- element.showIcon = true;
- assert.equal(dom(element.root)
- .querySelector('iron-icon').hidden, false);
+ test('normal position', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 100, width: 200};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 50},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isFalse(tooltip.updateStyles.called);
+ assert.equal(tooltip.style.left, '175px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('left side position', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 10, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '0px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('right side position', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 950, width: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '100px');
+ });
+
+ test('position to bottom', () => {
+ sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
+ return {top: 100, left: 950, width: 50, height: 50};
+ });
+ const tooltip = makeTooltip(
+ {height: 30, width: 120},
+ {top: 0, left: 0, width: 1000});
+
+ element.positionBelow = true;
+ element._positionTooltip(tooltip);
+ assert.isTrue(tooltip.updateStyles.called);
+ const offset = tooltip.updateStyles
+ .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
+ assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.equal(tooltip.style.left, '915px');
+ assert.equal(tooltip.style.top, '157.2px');
+ });
+
+ test('hides tooltip when detached', async () => {
+ sinon.stub(element, '_handleHideTooltip');
+ element.remove();
+ await element.updateComplete;
+ assert.isTrue(element._handleHideTooltip.called);
+ });
+
+ test('sets up listeners when has-tooltip is changed', async () => {
+ const addListenerStub = sinon.stub(element, 'addEventListener');
+ element.hasTooltip = true;
+ await element.updateComplete;
+ assert.isTrue(addListenerStub.called);
+ });
+
+ test('clean up listeners when has-tooltip changed to false', async () => {
+ const removeListenerStub = sinon.stub(element, 'removeEventListener');
+ element.hasTooltip = true;
+ await element.updateComplete;
+ element.hasTooltip = false;
+ await element.updateComplete;
+ assert.isTrue(removeListenerStub.called);
+ });
+
+ test('do not display tooltips on touch devices', async () => {
+ // On touch devices, tooltips should not be shown.
+ element.isTouchDevice = true;
+ await element.updateComplete;
+
+ // fire mouse-enter
+ element._handleShowTooltip();
+ await element.updateComplete;
+ assert.isNotOk(element.tooltip);
+
+ // fire mouse-enter
+ element._handleHideTooltip();
+ await element.updateComplete;
+ assert.isNotOk(element.tooltip);
+
+ // On other devices, tooltips should be shown.
+ element.isTouchDevice = false;
+
+ // fire mouse-enter
+ element._handleShowTooltip();
+ await element.updateComplete;
+ assert.isOk(element.tooltip);
+
+ // fire mouse-enter
+ element._handleHideTooltip();
+ await element.updateComplete;
+ assert.isNotOk(element.tooltip);
});
});
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
deleted file mode 100644
index 3e20d1d..0000000
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin.ts
+++ /dev/null
@@ -1,227 +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 '../../elements/shared/gr-tooltip/gr-tooltip';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {getRootElement} from '../../scripts/rootElement';
-import {property, observe} from '@polymer/decorators';
-import {GrTooltip} from '../../elements/shared/gr-tooltip/gr-tooltip';
-import {PolymerElement} from '@polymer/polymer';
-import {Constructor} from '../../utils/common-util';
-
-const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip.
-
-/** The interface corresponding to TooltipMixin */
-export interface TooltipMixinInterface {
- hasTooltip: boolean;
- positionBelow: boolean;
- _isTouchDevice: boolean;
- _tooltip: GrTooltip | null;
- _titleText: string;
- _hasSetupTooltipListeners: boolean;
-}
-
-/**
- * @polymer
- * @mixinFunction
- */
-export const TooltipMixin = <T extends Constructor<PolymerElement>>(
- superClass: T
-) => {
- /**
- * @polymer
- * @mixinClass
- */
- class Mixin extends superClass {
- @property({type: Boolean})
- hasTooltip = false;
-
- @property({type: Boolean, reflectToAttribute: true})
- positionBelow = false;
-
- @property({type: Boolean})
- _isTouchDevice = 'ontouchstart' in document.documentElement;
-
- @property({type: Object})
- _tooltip: GrTooltip | null = null;
-
- @property({type: String})
- _titleText = '';
-
- @property({type: Boolean})
- _hasSetupTooltipListeners = false;
-
- // Handler for mouseenter event
- private mouseenterHandler?: (e: MouseEvent) => void;
-
- // Handler for scrolling on window
- private readonly windowScrollHandler: () => void;
-
- // Handler for showing the tooltip, will be attached to certain events
- private readonly showHandler: () => void;
-
- // Handler for hiding the tooltip, will be attached to certain events
- private readonly hideHandler: (e: Event) => void;
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
- constructor(..._: any[]) {
- super();
- this.windowScrollHandler = () => this._handleWindowScroll();
- this.showHandler = () => this._handleShowTooltip();
- this.hideHandler = (e: Event | undefined) => this._handleHideTooltip(e);
- }
-
- override disconnectedCallback() {
- // NOTE: if you define your own `detached` in your component
- // then this won't take affect (as its not a class yet)
- this._handleHideTooltip(undefined);
- if (this.mouseenterHandler) {
- this.removeEventListener('mouseenter', this.mouseenterHandler);
- }
- window.removeEventListener('scroll', this.windowScrollHandler);
- super.disconnectedCallback();
- }
-
- @observe('hasTooltip')
- _setupTooltipListeners() {
- if (!this.mouseenterHandler) {
- this.mouseenterHandler = this.showHandler;
- }
-
- if (!this.hasTooltip) {
- // if attribute set to false, remove the listener
- this.removeEventListener('mouseenter', this.mouseenterHandler);
- this._hasSetupTooltipListeners = false;
- return;
- }
-
- if (this._hasSetupTooltipListeners) {
- return;
- }
- this._hasSetupTooltipListeners = true;
-
- this.addEventListener('mouseenter', this.mouseenterHandler);
- }
-
- _handleShowTooltip() {
- if (this._isTouchDevice) {
- return;
- }
-
- if (
- !this.hasAttribute('title') ||
- this.getAttribute('title') === '' ||
- this._tooltip
- ) {
- return;
- }
-
- // Store the title attribute text then set it to an empty string to
- // prevent it from showing natively.
- this._titleText = this.getAttribute('title') || '';
- this.setAttribute('title', '');
-
- const tooltip = document.createElement('gr-tooltip');
- tooltip.text = this._titleText;
- tooltip.maxWidth = this.getAttribute('max-width') || '';
- tooltip.positionBelow = this.hasAttribute('position-below');
-
- // Set visibility to hidden before appending to the DOM so that
- // calculations can be made based on the element’s size.
- tooltip.style.visibility = 'hidden';
- getRootElement().appendChild(tooltip);
- this._positionTooltip(tooltip);
- tooltip.style.visibility = 'initial';
-
- this._tooltip = tooltip;
- window.addEventListener('scroll', this.windowScrollHandler);
- this.addEventListener('mouseleave', this.hideHandler);
- this.addEventListener('click', this.hideHandler);
- tooltip.addEventListener('mouseleave', this.hideHandler);
- }
-
- _handleHideTooltip(e: Event | undefined) {
- if (this._isTouchDevice) {
- return;
- }
- if (!this.hasAttribute('title') || !this._titleText) {
- return;
- }
- // Do not hide if mouse left this or this._tooltip and came to this or
- // this._tooltip
- if (
- (e as MouseEvent)?.relatedTarget === this._tooltip ||
- (e as MouseEvent)?.relatedTarget === this
- ) {
- return;
- }
-
- window.removeEventListener('scroll', this.windowScrollHandler);
- this.removeEventListener('mouseleave', this.hideHandler);
- this.removeEventListener('click', this.hideHandler);
- this.setAttribute('title', this._titleText);
- this._tooltip?.removeEventListener('mouseleave', this.hideHandler);
-
- if (this._tooltip?.parentNode) {
- this._tooltip.parentNode.removeChild(this._tooltip);
- }
- this._tooltip = null;
- }
-
- _handleWindowScroll() {
- if (!this._tooltip) {
- return;
- }
-
- this._positionTooltip(this._tooltip);
- }
-
- _positionTooltip(tooltip: GrTooltip) {
- // This flush is needed for tooltips to be positioned correctly in Firefox
- // and Safari.
- flush();
- const rect = this.getBoundingClientRect();
- const boxRect = tooltip.getBoundingClientRect();
- if (!tooltip.parentElement) {
- return;
- }
- const parentRect = tooltip.parentElement.getBoundingClientRect();
- const top = rect.top - parentRect.top;
- const left =
- rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
- const right = parentRect.width - left - boxRect.width;
- if (left < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${left}px`,
- });
- } else if (right < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
- });
- }
- tooltip.style.left = `${Math.max(0, left)}px`;
-
- if (!this.positionBelow) {
- tooltip.style.top = `${Math.max(0, top)}px`;
- tooltip.style.transform = `translateY(calc(-100% - ${BOTTOM_OFFSET}px))`;
- } else {
- tooltip.style.top = `${top + rect.height + BOTTOM_OFFSET}px`;
- }
- }
- }
-
- return Mixin as T & Constructor<TooltipMixinInterface>;
-};
diff --git a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js b/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
deleted file mode 100644
index 69e6e86..0000000
--- a/polygerrit-ui/app/mixins/gr-tooltip-mixin/gr-tooltip-mixin_test.js
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * @license
- * Copyright (C) 2017 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 '../../test/common-test-setup-karma.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {TooltipMixin} from './gr-tooltip-mixin.js';
-
-const basicFixture = fixtureFromElement('gr-tooltip-mixin-element');
-
-// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = TooltipMixin(PolymerElement);
-
-class GrTooltipMixinTestElement extends base {
- static get is() {
- return 'gr-tooltip-mixin-element';
- }
-}
-
-customElements.define(GrTooltipMixinTestElement.is,
- GrTooltipMixinTestElement);
-
-suite('gr-tooltip-mixin tests', () => {
- let element;
-
- function makeTooltip(tooltipRect, parentRect) {
- return {
- getBoundingClientRect() { return tooltipRect; },
- updateStyles: sinon.stub(),
- style: {left: 0, top: 0},
- parentElement: {
- getBoundingClientRect() { return parentRect; },
- },
- };
- }
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('normal position', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 100, width: 200};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 50},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isFalse(tooltip.updateStyles.called);
- assert.equal(tooltip.style.left, '175px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('left side position', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 10, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '0px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('right side position', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 950, width: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '100px');
- });
-
- test('position to bottom', () => {
- sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
- return {top: 100, left: 950, width: 50, height: 50};
- });
- const tooltip = makeTooltip(
- {height: 30, width: 120},
- {top: 0, left: 0, width: 1000});
-
- element.positionBelow = true;
- element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
- assert.equal(tooltip.style.left, '915px');
- assert.equal(tooltip.style.top, '157.2px');
- });
-
- test('hides tooltip when detached', () => {
- sinon.stub(element, '_handleHideTooltip');
- element.remove();
- flush();
- assert.isTrue(element._handleHideTooltip.called);
- });
-
- test('sets up listeners when has-tooltip is changed', () => {
- const addListenerStub = sinon.stub(element, 'addEventListener');
- element.hasTooltip = true;
- assert.isTrue(addListenerStub.called);
- });
-
- test('clean up listeners when has-tooltip changed to false', () => {
- const removeListenerStub = sinon.stub(element, 'removeEventListener');
- element.hasTooltip = true;
- element.hasTooltip = false;
- assert.isTrue(removeListenerStub.called);
- });
-});
-