/** * Copyright Aquera Inc 2023 * * This source code is licensed under the BSD-3-Clause license found in the * LICENSE file in the root directory of this source tree. */ import { LitElement, html, CSSResultArray, TemplateResult, PropertyValues, } from 'lit-element'; import { customElement,property, state } from 'lit/decorators.js'; import { styles } from './nile-popover.css'; import NileElement from '../internal/nile-element'; import { watch } from '../internal/watch'; import { CSSResultGroup } from 'lit'; import { PopoverPortalManager } from './portal-manager'; import { VisibilityManager } from '../utilities/visibility-manager.js'; /** * Nile icon component. * * @tag nile-popover * */ @customElement('nile-popover') export class NilePopover extends NileElement { /** * The styles for Popover * @remarks If you are extending this class you can extend the base styles with super. Eg `return [super(), myCustomStyles]` */ public static get styles(): CSSResultGroup { return [styles]; } /** * The preferred placement of the popover. Note that the actual placement may vary as needed to keep the tooltip * inside of the viewport. */ @property() placement: | 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' = 'top'; /** The distance in pixels from which to offset the popover away from its target. */ @property({ type: Number }) distance = 18; @property({ type: Boolean, reflect: true }) preventOverlayClose = false; @property({ type: Boolean, attribute: 'arrow' }) arrow = true; /** Gives the title to the popover */ @property({ type: String }) title = ''; @property({type:Boolean }) open = false; @state() isShow:boolean = false; @property({ attribute: 'arrow-placement' }) arrowPlacement: | 'start' | 'end' | 'center' | 'anchor' = 'anchor'; private portalManager: PopoverPortalManager | null = null; private visibilityManager?: VisibilityManager; @property({ type: Boolean, reflect: true }) enableVisibilityEffect = false; @property({ type: Boolean, reflect: true }) enableTabClose = false; /** * Enable this option to prevent the panel from being clipped when the component is placed inside a container with * `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios. */ @property({ type: Boolean }) hoist = false; /** * Enable portal mode to render the popover content in a portal outside the component's DOM tree. * This provides better positioning control and prevents clipping issues in complex layouts. */ @property({ type: Boolean }) portal = false; @property({ type: Boolean }) flip = false; /* #endregion */ /* #region Methods */ /** * Render method * @slot This is a slot test */ connectedCallback() { super.connectedCallback(); this.emit('nile-init'); document.addEventListener('click', this.handleDocumentClick); requestAnimationFrame(() => { if (this.portal && !this.portalManager) { this.portalManager = new PopoverPortalManager(this); } }); } disconnectedCallback() { super.disconnectedCallback(); this.emit('nile-destroy'); this.visibilityManager?.cleanup(); document.removeEventListener('click', this.handleDocumentClick); if (this.portalManager) { this.portalManager.cleanupPortalAppend(); this.portalManager = null; } } protected async firstUpdated(_changed: PropertyValues) { await this.updateComplete; const anchorSlot = this.renderRoot.querySelector('slot[name="anchor"]') as HTMLSlotElement | null; const anchorEl = anchorSlot?.assignedElements({ flatten: true })[0] as HTMLElement | undefined; this.visibilityManager = new VisibilityManager({ host: this, target: anchorEl || null, enableVisibilityEffect: this.enableVisibilityEffect, enableTabClose: this.enableTabClose, isOpen: () => this.isShow, onAnchorOutOfView: () => { this.isShow = false; this.emit('nile-visibility-change', { visible: false, reason: 'anchor-out-of-view', }); }, onDocumentHidden: () => { this.isShow = false; this.emit('nile-visibility-change', { visible: false, reason: 'document-hidden', }); }, emit: (event, detail) => this.emit(`nile-${event}`, detail), }); } protected updated(_changedProperties: PropertyValues): void { if (_changedProperties.has('open')) { this.isShow = this.open; } if (_changedProperties.has('portal')) { if (this.portal && !this.portalManager) { this.portalManager = new PopoverPortalManager(this); } else if (!this.portal && this.portalManager) { this.portalManager.cleanupPortalAppend(); this.portalManager = null; } } } @watch('isShow') handleShowHide() { if (this.isShow) { this.emit('nile-show'); this.visibilityManager?.setup(); if (this.portal && this.portalManager) { this.portalManager.setupPortalAppend(); } else if (this.portal && !this.portalManager) { this.portalManager = new PopoverPortalManager(this); this.portalManager.setupPortalAppend(); } } else { this.emit('nile-hide'); this.visibilityManager?.cleanup(); if (this.portal && this.portalManager) { this.portalManager.cleanupPortalAppend(); } } } public render(): TemplateResult { const shouldRenderContent = this.isShow && !(this.portal && this.portalManager); return html` e.stopPropagation()} arrowPlacement="${this.arrowPlacement}" .flip="${this.flip}" shift strategy=${this.hoist ? 'fixed' : 'absolute'} > ${shouldRenderContent ? html`
${this.title}
` : html``}
`; } private handleClick = () => { this.isShow = !this.isShow; const allPopovers = document.querySelectorAll('nile-popover'); allPopovers.forEach(popover => { if (popover !== this) { popover.isShow = false; } }); }; private handleDocumentClick = () => { if (this.isShow && !this.preventOverlayClose) { this.isShow = false; } }; public updatePortalContent(): void { if (this.portalManager) { this.portalManager.updatePortalAppendPosition(); } } public forceReposition(): void { if (this.portalManager) { this.portalManager.forceReposition(); } } public getCurrentPlacement(): string { if (this.portalManager) { return this.portalManager.getCurrentPlacement(); } return this.placement; } public isPositioningOptimal(): boolean { if (this.portalManager) { return this.portalManager.isPositioningOptimal(); } return true; } /* #endregion */ } export default NilePopover; declare global { interface HTMLElementTagNameMap { 'nile-popover': NilePopover; } }