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; -}