Merge "Submit requirements - do not render neutral gr-vote-chip"
diff --git a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
index 3c20ff7..8f5af76 100644
--- a/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
+++ b/java/com/google/gerrit/extensions/client/ProjectWatchInfo.java
@@ -54,6 +54,7 @@
}
@Override
+ @SuppressWarnings("OrphanedFormatString")
public String toString() {
StringBuilder b = new StringBuilder();
b.append(project);
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index 4b1dba6..3c9f54c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -14,19 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-hovercard-styles';
-import '../../../styles/shared-styles';
import '../../shared/gr-button/gr-button';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {customElement, property} from '@polymer/decorators';
-import {HovercardBehaviorMixin} from '../../shared/gr-hovercard/gr-hovercard-behavior';
-import {htmlTemplate} from './gr-submit-requirement-hovercard_html';
+import '../../shared/gr-label-info/gr-label-info';
+import '../../shared/gr-limited-text/gr-limited-text';
+import {customElement, property} from 'lit/decorators';
import {
AccountInfo,
SubmitRequirementExpressionInfo,
SubmitRequirementResultInfo,
- SubmitRequirementStatus,
} from '../../../api/rest-api';
import {
extractAssociatedLabels,
@@ -34,16 +29,15 @@
} from '../../../utils/label-util';
import {ParsedChangeInfo} from '../../../types/types';
import {Label} from '../gr-change-requirements/gr-change-requirements';
+import {css, html, LitElement} from 'lit';
+import {HovercardMixin} from '../../../mixins/hovercard-mixin/hovercard-mixin';
+import {fontStyles} from '../../../styles/gr-font-styles';
// This avoids JSC_DYNAMIC_EXTENDS_WITHOUT_JSDOC closure compiler error.
-const base = HovercardBehaviorMixin(PolymerElement);
+const base = HovercardMixin(LitElement);
@customElement('gr-submit-requirement-hovercard')
export class GrHovercardRun extends base {
- static get template() {
- return htmlTemplate;
- }
-
@property({type: Object})
requirement?: SubmitRequirementResultInfo;
@@ -59,16 +53,176 @@
@property({type: Boolean})
expanded = false;
- @property({type: Array, computed: 'computeLabels(change, requirement)'})
- _labels: Label[] = [];
+ static override get styles() {
+ return [
+ fontStyles,
+ base.styles || [],
+ css`
+ #container {
+ min-width: 356px;
+ max-width: 356px;
+ padding: var(--spacing-xl) 0 var(--spacing-m) 0;
+ }
+ section.label {
+ display: table-row;
+ }
+ .label-title {
+ min-width: 10em;
+ padding-top: var(--spacing-s);
+ }
+ .label-value {
+ padding-top: var(--spacing-s);
+ }
+ .label-title,
+ .label-value {
+ display: table-cell;
+ vertical-align: top;
+ }
+ .row {
+ display: flex;
+ }
+ .title {
+ color: var(--deemphasized-text-color);
+ margin-right: var(--spacing-m);
+ }
+ div.section {
+ margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
+ display: flex;
+ align-items: center;
+ }
+ div.sectionIcon {
+ flex: 0 0 30px;
+ }
+ div.sectionIcon iron-icon {
+ position: relative;
+ width: 20px;
+ height: 20px;
+ }
+ .condition {
+ background-color: var(--gray-background);
+ padding: var(--spacing-m);
+ flex-grow: 1;
+ }
+ .expression {
+ color: var(--gray-foreground);
+ }
+ iron-icon.check {
+ color: var(--success-foreground);
+ }
+ iron-icon.close {
+ color: var(--warning-foreground);
+ }
+ .showConditions iron-icon {
+ color: inherit;
+ }
+ div.showConditions {
+ border-top: 1px solid var(--border-color);
+ margin-top: var(--spacing-m);
+ padding: var(--spacing-m) var(--spacing-xl) 0;
+ }
+ `,
+ ];
+ }
- computeLabels(
- change?: ParsedChangeInfo,
- requirement?: SubmitRequirementResultInfo
- ) {
- if (!requirement) return [];
- const requirementLabels = extractAssociatedLabels(requirement);
- const labels = change?.labels ?? {};
+ override render() {
+ if (!this.requirement) return;
+ const icon = iconForStatus(this.requirement.status);
+ return html` <div id="container" role="tooltip" tabindex="-1">
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="${icon}" icon="gr-icons:${icon}"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ <h3 class="name heading-3">
+ <span>${this.requirement.name}</span>
+ </h3>
+ </div>
+ </div>
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
+ </div>
+ <div class="sectionContent">
+ <div class="row">
+ <div class="title">Status</div>
+ <div>${this.requirement.status}</div>
+ </div>
+ </div>
+ </div>
+ ${this.renderLabelSection()} ${this.renderConditionSection()}
+ </div>`;
+ }
+
+ private renderLabelSection() {
+ const labels = this.computeLabels();
+ return html` <div class="section">
+ ${labels.map(l => this.renderLabel(l))}
+ </div>`;
+ }
+
+ private renderLabel(label: Label) {
+ return html`
+ <section class="label">
+ <div class="label-title">
+ <gr-limited-text
+ class="name"
+ limit="25"
+ text="${label.labelName}"
+ ></gr-limited-text>
+ </div>
+ <div class="label-value">
+ <gr-label-info
+ .change=${this.change}
+ .account=${this.account}
+ .mutable=${this.mutable}
+ .label="${label.labelName}"
+ .labelInfo="${label.labelInfo}"
+ ></gr-label-info>
+ </div>
+ </section>
+ `;
+ }
+
+ private renderConditionSection() {
+ if (!this.expanded) {
+ return html` <div class="showConditions">
+ <gr-button
+ link=""
+ class="showConditions"
+ @click="${(_: MouseEvent) => this.handleShowConditions()}"
+ >
+ View condition
+ <iron-icon icon="gr-icons:expand-more"></iron-icon
+ ></gr-button>
+ </div>`;
+ } else {
+ return html`
+ <div class="section">
+ <div class="sectionIcon">
+ <iron-icon icon="gr-icons:description"></iron-icon>
+ </div>
+ <div class="sectionContent">${this.requirement?.description}</div>
+ </div>
+ ${this.renderCondition(
+ 'Blocking condition',
+ this.requirement?.submittability_expression_result
+ )}
+ ${this.renderCondition(
+ 'Application condition',
+ this.requirement?.applicability_expression_result
+ )}
+ ${this.renderCondition(
+ 'Override condition',
+ this.requirement?.override_expression_result
+ )}
+ `;
+ }
+ }
+
+ private computeLabels() {
+ if (!this.requirement) return [];
+ const requirementLabels = extractAssociatedLabels(this.requirement);
+ const labels = this.change?.labels ?? {};
const allLabels: Label[] = [];
@@ -85,17 +239,23 @@
return allLabels;
}
- computeIcon(status: SubmitRequirementStatus) {
- return iconForStatus(status);
- }
-
- renderCondition(expression?: SubmitRequirementExpressionInfo) {
+ private renderCondition(
+ name: string,
+ expression?: SubmitRequirementExpressionInfo
+ ) {
if (!expression) return '';
-
- return expression.expression;
+ return html`
+ <div class="section">
+ <div class="sectionIcon"></div>
+ <div class="sectionContent condition">
+ ${name}:<br />
+ <span class="expression"> ${expression.expression} </span>
+ </div>
+ </div>
+ `;
}
- _handleShowConditions() {
+ private handleShowConditions() {
this.expanded = true;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
deleted file mode 100644
index 5023895..0000000
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_html.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/**
- * @license
- * Copyright (C) 2021 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 include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-hovercard-styles">
- #container {
- min-width: 356px;
- max-width: 356px;
- padding: var(--spacing-xl) 0 var(--spacing-m) 0;
- }
- section.label {
- display: table-row;
- }
- .label-title {
- min-width: 10em;
- padding-top: var(--spacing-s);
- }
- .label-value {
- padding-top: var(--spacing-s);
- }
- .label-title,
- .label-value {
- display: table-cell;
- vertical-align: top;
- }
- .row {
- display: flex;
- }
- .title {
- color: var(--deemphasized-text-color);
- margin-right: var(--spacing-m);
- }
- div.section {
- margin: 0 var(--spacing-xl) var(--spacing-m) var(--spacing-xl);
- display: flex;
- align-items: center;
- }
- div.sectionIcon {
- flex: 0 0 30px;
- }
- div.sectionIcon iron-icon {
- position: relative;
- width: 20px;
- height: 20px;
- }
- .condition {
- background-color: var(--gray-background);
- padding: var(--spacing-m);
- flex-grow: 1;
- }
- .expression {
- color: var(--gray-foreground);
- }
- iron-icon.check {
- color: var(--success-foreground);
- }
- iron-icon.close {
- color: var(--warning-foreground);
- }
- .showConditions iron-icon {
- color: inherit;
- }
- div.showConditions {
- border-top: 1px solid var(--border-color);
- margin-top: var(--spacing-m);
- padding: var(--spacing-m) var(--spacing-xl) 0;
- }
- </style>
- <div id="container" role="tooltip" tabindex="-1">
- <div class="section">
- <div class="sectionIcon">
- <iron-icon
- class$="[[computeIcon(requirement.status)]]"
- icon="gr-icons:[[computeIcon(requirement.status)]]"
- ></iron-icon>
- </div>
- <div class="sectionContent">
- <h3 class="name heading-3">
- <span>[[requirement.name]]</span>
- </h3>
- </div>
- </div>
- <div class="section">
- <div class="sectionIcon">
- <iron-icon class="small" icon="gr-icons:info-outline"></iron-icon>
- </div>
- <div class="sectionContent">
- <div class="row">
- <div class="title">Status</div>
- <div>[[requirement.status]]</div>
- </div>
- </div>
- </div>
- <div class="section">
- <template is="dom-repeat" items="[[_labels]]">
- <section class="label">
- <div class="label-title">
- <gr-limited-text
- class="name"
- limit="25"
- text="[[item.labelName]]"
- ></gr-limited-text>
- </div>
- <div class="label-value">
- <gr-label-info
- change="{{change}}"
- account="[[account]]"
- mutable="[[mutable]]"
- label="[[item.labelName]]"
- label-info="[[item.labelInfo]]"
- ></gr-label-info>
- </div>
- </section>
- </template>
- </div>
- <template is="dom-if" if="[[!expanded]]">
- <div class="showConditions">
- <gr-button
- link=""
- class="showConditions"
- on-click="_handleShowConditions"
- >
- View condition
- <iron-icon icon="gr-icons:expand-more"></iron-icon
- ></gr-button>
- </div>
- </template>
- <template is="dom-if" if="[[expanded]]">
- <div class="section">
- <div class="sectionIcon">
- <iron-icon icon="gr-icons:description"></iron-icon>
- </div>
- <div class="sectionContent">[[requirement.description]]</div>
- </div>
- <div class="section">
- <div class="sectionIcon"></div>
- <div class="sectionContent condition">
- Blocking condition:<br />
- <span class="expression">
- [[renderCondition(requirement.submittability_expression_result)]]
- </span>
- </div>
- </div>
- <template
- is="dom-if"
- if="[[requirement.applicability_expression_result]]"
- >
- <div class="section">
- <div class="sectionIcon"></div>
- <div class="sectionContent condition">
- Application condition:<br />
- <span class="expression">
- [[renderCondition(requirement.applicability_expression_result)]]
- </span>
- </div>
- </div>
- </template>
- <template is="dom-if" if="[[requirement.override_expression_result]]">
- <div class="section">
- <div class="sectionIcon"></div>
- <div class="sectionContent condition">
- Override condition:<br />
- <span class="expression">
- [[renderCondition(requirement.override_expression_result)]]
- </span>
- </div>
- </div>
- </template>
- </template>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
index c2b54a6..004d594 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirements/gr-submit-requirements.ts
@@ -81,20 +81,6 @@
iron-icon.close {
color: var(--warning-foreground);
}
- .testing {
- margin-top: var(--spacing-xxl);
- padding-left: var(--metadata-horizontal-padding);
- color: var(--deemphasized-text-color);
- }
- .testing gr-button {
- min-width: 25px;
- }
- .testing * {
- visibility: hidden;
- }
- .testing:hover * {
- visibility: visible;
- }
.requirements,
section.trigger-votes {
margin-left: var(--spacing-l);
@@ -193,9 +179,7 @@
></gr-submit-requirement-hovercard>
`
)}
- ${this.renderTriggerVotes(
- submit_requirements
- )}${this.renderFakeControls()}`;
+ ${this.renderTriggerVotes(submit_requirements)}`;
}
renderStatus(status: SubmitRequirementStatus) {
@@ -291,49 +275,6 @@
)}
</section>`;
}
-
- renderFakeControls() {
- return html`
- <div class="testing">
- <div>Toggle fake data:</div>
- <gr-button link @click="${() => this.renderFakeSubmitRequirements()}"
- >G</gr-button
- >
- </div>
- `;
- }
-
- renderFakeSubmitRequirements() {
- if (!this.change) return;
- this.change = {
- ...this.change,
- submit_requirements: [
- {
- name: 'Code-Review',
- status: SubmitRequirementStatus.SATISFIED,
- description:
- "At least one maximum vote for label 'Code-Review' is required",
- submittability_expression_result: {
- expression: 'label:Code-Review=MAX -label:Code-Review=MIN',
- fulfilled: true,
- passing_atoms: [],
- failing_atoms: [],
- },
- },
- {
- name: 'Verified',
- status: SubmitRequirementStatus.UNSATISFIED,
- description: 'CI build and tests results are verified',
- submittability_expression_result: {
- expression: 'label:Verified=MAX -label:Verified=MIN',
- fulfilled: false,
- passing_atoms: [],
- failing_atoms: [],
- },
- },
- ],
- };
- }
}
@customElement('gr-trigger-vote')
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
deleted file mode 100644
index 7edb728a..0000000
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.ts
+++ /dev/null
@@ -1,487 +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 '../../../styles/shared-styles';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {getRootElement} from '../../../scripts/rootElement';
-import {Constructor} from '../../../utils/common-util';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {observe, property} from '@polymer/decorators';
-import {
- pushScrollLock,
- removeScrollLock,
-} from '@polymer/iron-overlay-behavior/iron-scroll-manager';
-import {ShowAlertEventDetail} from '../../../types/events';
-import {debounce, DelayedTask} from '../../../utils/async-util';
-interface ReloadEventDetail {
- clearPatchset?: boolean;
-}
-
-const HOVER_CLASS = 'hovered';
-const HIDE_CLASS = 'hide';
-
-/**
- * ID for the container element.
- */
-const containerId = 'gr-hovercard-container';
-
-function getHovercardContainer(
- options: {createIfNotExists: boolean} = {createIfNotExists: false}
-): HTMLElement | null {
- let container = getRootElement().querySelector<HTMLElement>(
- `#${containerId}`
- );
- if (!container && options.createIfNotExists) {
- // If it does not exist, create and initialize the hovercard container.
- container = document.createElement('div');
- container.setAttribute('id', containerId);
- getRootElement().appendChild(container);
- }
- return container;
-}
-
-/**
- * How long should we wait before showing the hovercard when the user hovers
- * over the element?
- */
-const SHOW_DELAY_MS = 550;
-
-/**
- * How long should we wait before hiding the hovercard when the user moves from
- * target to the hovercard.
- *
- * Note: this should be lower than SHOW_DELAY_MS to avoid flickering.
- */
-const HIDE_DELAY_MS = 500;
-
-/**
- * The mixin for gr-hovercard-behavior.
- *
- * @example
- *
- * class YourComponent extends hovercardBehaviorMixin(
- * PolymerElement
- *
- * @see gr-hovercard.ts
- *
- * // following annotations are required for polylint
- * @polymer
- * @mixinFunction
- */
-export const HovercardBehaviorMixin = <T extends Constructor<PolymerElement>>(
- superClass: T
-) => {
- /**
- * @polymer
- * @mixinClass
- */
- class Mixin extends superClass {
- @property({type: Object})
- _target: HTMLElement | null = null;
-
- // Determines whether or not the hovercard is visible.
- @property({type: Boolean})
- _isShowing = false;
-
- // The `id` of the element that the hovercard is anchored to.
- @property({type: String})
- for?: string;
-
- /**
- * The spacing between the top of the hovercard and the element it is
- * anchored to.
- */
- @property({type: Number})
- offset = 14;
-
- /**
- * Positions the hovercard to the top, right, bottom, left, bottom-left,
- * bottom-right, top-left, or top-right of its content.
- */
- @property({type: String})
- position = 'right';
-
- @property({type: Object})
- container: HTMLElement | null = null;
-
- private hideTask?: DelayedTask;
-
- private showTask?: DelayedTask;
-
- private isScheduledToShow?: boolean;
-
- private isScheduledToHide?: boolean;
-
- override connectedCallback() {
- super.connectedCallback();
- if (!this._target) {
- this._target = this.target;
- this.addTargetEventListeners();
- }
-
- // show the hovercard if mouse moves to hovercard
- // this will cancel pending hide as well
- this.addEventListener('mouseenter', this.show);
- this.addEventListener('mouseenter', this.lock);
- // when leave hovercard, hide it immediately
- this.addEventListener('mouseleave', this.hide);
- this.addEventListener('mouseleave', this.unlock);
- }
-
- override disconnectedCallback() {
- this.cancelShowTask();
- this.cancelHideTask();
- this.unlock();
- super.disconnectedCallback();
- }
-
- addTargetEventListeners() {
- this._target?.addEventListener('mouseenter', this.debounceShow);
- this._target?.addEventListener('focus', this.debounceShow);
- this._target?.addEventListener('mouseleave', this.debounceHide);
- this._target?.addEventListener('blur', this.debounceHide);
- this._target?.addEventListener('click', this.hide);
- }
-
- removeTargetEventListeners() {
- this._target?.removeEventListener('mouseenter', this.debounceShow);
- this._target?.removeEventListener('focus', this.debounceShow);
- this._target?.removeEventListener('mouseleave', this.debounceHide);
- this._target?.removeEventListener('blur', this.debounceHide);
- this._target?.removeEventListener('click', this.hide);
- }
-
- override ready() {
- super.ready();
- // First, check to see if the container has already been created.
- this.container = getHovercardContainer({createIfNotExists: true});
- }
-
- readonly debounceHide = () => {
- this.cancelShowTask();
- if (!this._isShowing || this.isScheduledToHide) return;
- this.isScheduledToHide = true;
- this.hideTask = debounce(
- this.hideTask,
- () => {
- // This happens when hide immediately through click or mouse leave
- // on the hovercard
- if (!this.isScheduledToHide) return;
- this.hide();
- },
- HIDE_DELAY_MS
- );
- };
-
- cancelHideTask() {
- if (!this.hideTask) return;
- this.hideTask.cancel();
- this.isScheduledToHide = false;
- this.hideTask = undefined;
- }
-
- /**
- * Hovercard elements are created outside of <gr-app>, so if you want to fire
- * events, then you probably want to do that through the target element.
- */
-
- dispatchEventThroughTarget(eventName: string): void;
-
- dispatchEventThroughTarget(
- eventName: 'show-alert',
- detail: ShowAlertEventDetail
- ): void;
-
- dispatchEventThroughTarget(
- eventName: 'reload',
- detail: ReloadEventDetail
- ): void;
-
- dispatchEventThroughTarget(eventName: string, detail?: unknown) {
- if (!detail) detail = {};
- if (this._target)
- this._target.dispatchEvent(
- new CustomEvent(eventName, {
- detail,
- bubbles: true,
- composed: true,
- })
- );
- }
-
- /**
- * Returns the target element that the hovercard is anchored to (the `id` of
- * the `for` property).
- */
- get target(): HTMLElement {
- const parentNode = this.parentNode;
- // If the parentNode is a document fragment, then we need to use the host.
- const ownerRoot = this.getRootNode() as ShadowRoot;
- let target;
- if (this.for) {
- target = ownerRoot.querySelector('#' + this.for);
- } else {
- target =
- !parentNode || parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
- ? ownerRoot.host
- : parentNode;
- }
- return target as HTMLElement;
- }
-
- /**
- * unlock scroll, this will resume the scroll outside of the hovercard.
- */
- readonly unlock = () => {
- removeScrollLock(this);
- };
-
- /**
- * Hides/closes the hovercard. This occurs when the user triggers the
- * `mouseleave` event on the hovercard's `target` element (as long as the
- * user is not hovering over the hovercard).
- *
- */
- readonly hide = (e?: MouseEvent) => {
- this.cancelHideTask();
- this.cancelShowTask();
- if (!this._isShowing) {
- return;
- }
-
- // If the user is now hovering over the hovercard or the user is returning
- // from the hovercard but now hovering over the target (to stop an annoying
- // flicker effect), just return.
- if (e) {
- if (
- e.relatedTarget === this ||
- (e.target === this && e.relatedTarget === this._target)
- ) {
- return;
- }
- }
-
- // Mark that the hovercard is not visible and do not allow focusing
- this._isShowing = false;
-
- // Clear styles in preparation for the next time we need to show the card
- this.classList.remove(HOVER_CLASS);
-
- // Reset and remove the hovercard from the DOM
- this.style.cssText = '';
- this.$['container'].setAttribute('tabindex', '-1');
-
- // Remove the hovercard from the container, given that it is still a child
- // of the container.
- if (this.container?.contains(this)) {
- this.container.removeChild(this);
- }
- };
-
- /**
- * Shows/opens the hovercard with a fixed delay.
- */
- readonly debounceShow = () => {
- this.debounceShowBy(SHOW_DELAY_MS);
- };
-
- /**
- * Shows/opens the hovercard with the given delay.
- */
- debounceShowBy(delayMs: number) {
- this.cancelHideTask();
- if (this._isShowing || this.isScheduledToShow) return;
- this.isScheduledToShow = true;
- this.showTask = debounce(
- this.showTask,
- () => {
- // This happens when the mouse leaves the target before the delay is over.
- if (!this.isScheduledToShow) return;
- this.show();
- },
- delayMs
- );
- }
-
- cancelShowTask() {
- if (!this.showTask) return;
- this.showTask.cancel();
- this.isScheduledToShow = false;
- this.showTask = undefined;
- }
-
- /**
- * Lock background scroll but enable scroll inside of current hovercard.
- */
- readonly lock = () => {
- pushScrollLock(this);
- };
-
- /**
- * Shows/opens the hovercard. This occurs when the user triggers the
- * `mousenter` event on the hovercard's `target` element.
- */
- readonly show = async () => {
- this.cancelHideTask();
- this.cancelShowTask();
- if (this._isShowing || !this.container) {
- return;
- }
-
- // Mark that the hovercard is now visible
- this._isShowing = true;
- this.setAttribute('tabindex', '0');
-
- // Add it to the DOM and calculate its position
- this.container.appendChild(this);
- // We temporarily hide the hovercard until we have found the correct
- // position for it.
- this.classList.add(HIDE_CLASS);
- this.classList.add(HOVER_CLASS);
- // Make sure that the hovercard actually rendered and all dom-if
- // statements processed, so that we can measure the (invisible)
- // hovercard properly in updatePosition().
- await flush();
- this.updatePosition();
- this.classList.remove(HIDE_CLASS);
- };
-
- updatePosition() {
- const positionsToTry = new Set([
- this.position,
- 'right',
- 'bottom-right',
- 'top-right',
- 'bottom',
- 'top',
- 'bottom-left',
- 'top-left',
- 'left',
- ]);
- for (const position of positionsToTry) {
- this.updatePositionTo(position);
- if (this._isInsideViewport()) return;
- }
- console.warn('Could not find a visible position for the hovercard.');
- }
-
- _isInsideViewport() {
- const thisRect = this.getBoundingClientRect();
- if (thisRect.top < 0) return false;
- if (thisRect.left < 0) return false;
- const docuRect = document.documentElement.getBoundingClientRect();
- if (thisRect.bottom > docuRect.height) return false;
- if (thisRect.right > docuRect.width) return false;
- return true;
- }
-
- /**
- * Updates the hovercard's position based the current position of the `target`
- * element.
- *
- * The hovercard is supposed to stay open if the user hovers over it.
- * To keep it open when the user moves away from the target, the bounding
- * rects of the target and hovercard must touch or overlap.
- *
- * NOTE: You do not need to directly call this method unless you need to
- * update the position of the tooltip while it is already visible (the
- * target element has moved and the tooltip is still open).
- */
- updatePositionTo(position: string) {
- if (!this._target) {
- return;
- }
-
- // Make sure that thisRect will not get any paddings and such included
- // in the width and height of the bounding client rect.
- this.style.cssText = '';
-
- const docuRect = document.documentElement.getBoundingClientRect();
- const targetRect = this._target.getBoundingClientRect();
- const thisRect = this.getBoundingClientRect();
-
- const targetLeft = targetRect.left - docuRect.left;
- const targetTop = targetRect.top - docuRect.top;
-
- let hovercardLeft;
- let hovercardTop;
-
- switch (position) {
- case 'top':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop - thisRect.height - this.offset;
- break;
- case 'bottom':
- hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
- hovercardTop = targetTop + targetRect.height + this.offset;
- break;
- case 'left':
- hovercardLeft = targetLeft - thisRect.width - this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- break;
- case 'right':
- hovercardLeft = targetLeft + targetRect.width + this.offset;
- hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
- break;
- case 'bottom-right':
- hovercardLeft = targetLeft + targetRect.width + this.offset;
- hovercardTop = targetTop;
- break;
- case 'bottom-left':
- hovercardLeft = targetLeft - thisRect.width - this.offset;
- hovercardTop = targetTop;
- break;
- case 'top-left':
- hovercardLeft = targetLeft - thisRect.width - this.offset;
- hovercardTop = targetTop + targetRect.height - thisRect.height;
- break;
- case 'top-right':
- hovercardLeft = targetLeft + targetRect.width + this.offset;
- hovercardTop = targetTop + targetRect.height - thisRect.height;
- break;
- }
-
- this.style.left = `${hovercardLeft}px`;
- this.style.top = `${hovercardTop}px`;
- }
-
- /**
- * Responds to a change in the `for` value and gets the updated `target`
- * element for the hovercard.
- */
- @observe('for')
- _forChanged() {
- this.removeTargetEventListeners();
- this._target = this.target;
- this.addTargetEventListeners();
- }
- }
-
- return Mixin as T & Constructor<GrHovercardBehaviorInterface>;
-};
-
-export interface GrHovercardBehaviorInterface {
- _target: HTMLElement | null;
- _isShowing: boolean;
- ready(): void;
- dispatchEventThroughTarget(eventName: string, detail?: unknown): void;
- hide(e?: MouseEvent): void;
- debounceShow(): void;
- debounceShowBy(delayMs: number): void;
- cancelShowTask(): void;
- show(): void;
- updatePosition(): void;
-}