Remove gr-hovercard-behavior
Google-Bug-Id: b/202457138
Change-Id: Id40b28bd5ba790b1a94f6c332d30986166aa622f
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;
-}