UNPKG

@vaadin/dialog

Version:
331 lines (282 loc) 10.3 kB
/** * @license * Copyright (c) 2017 - 2026 Vaadin Ltd. * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ */ import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js'; import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js'; import { setOverlayStateAttribute } from '@vaadin/overlay/src/vaadin-overlay-utils.js'; /** * @polymerMixin * @mixes OverlayMixin */ export const DialogOverlayMixin = (superClass) => class DialogOverlayMixin extends OverlayMixin(superClass) { static get properties() { return { /** * String used for rendering a dialog title. */ headerTitle: { type: String, }, /** * Custom function for rendering the dialog header. */ headerRenderer: { type: Object, }, /** * Custom function for rendering the dialog footer. */ footerRenderer: { type: Object, }, /** * Whether to keep the overlay within the viewport. */ keepInViewport: { type: Boolean, reflectToAttribute: true, }, }; } static get observers() { return [ '_headerFooterRendererChange(headerRenderer, footerRenderer, opened)', '_headerTitleChanged(headerTitle, opened)', ]; } /** * Override method from OverlayFocusMixin to use owner as content root * @protected * @override */ get _contentRoot() { return this.owner; } /** * Override method from OverlayMixin to use slotted div as a renderer root. * @protected * @override */ get _rendererRoot() { if (!this.__savedRoot) { const root = document.createElement('vaadin-dialog-content'); root.style.display = 'contents'; this.owner.appendChild(root); this.__savedRoot = root; } return this.__savedRoot; } /** @protected */ ready() { super.ready(); // Update overflow attribute on resize this.__resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => { this.__adjustPosition(); this.__updateOverflow(); }); }); this.__resizeObserver.observe(this.$.resizerContainer); // Update overflow attribute on scroll this.$.content.addEventListener('scroll', () => { this.__updateOverflow(); }); // Update overflow attribute on content change this.shadowRoot.addEventListener('slotchange', () => { this.__updateOverflow(); }); // Observe header-content and footer slots for dynamic content const headerSlot = this.shadowRoot.querySelector('slot[name="header-content"]'); this.__headerSlotObserver = new SlotObserver(headerSlot, ({ currentNodes }) => { setOverlayStateAttribute(this, 'has-header', currentNodes.length > 0); this.__updateOverflow(); }); const footerSlot = this.shadowRoot.querySelector('slot[name="footer"]'); this.__footerSlotObserver = new SlotObserver(footerSlot, ({ currentNodes }) => { setOverlayStateAttribute(this, 'has-footer', currentNodes.length > 0); this.__updateOverflow(); }); this.__handleWindowResize = this.__handleWindowResize.bind(this); } updated(props) { super.updated(props); if (props.has('opened') || props.has('keepInViewport')) { if (this.opened && this.keepInViewport) { window.addEventListener('resize', this.__handleWindowResize); } else { window.removeEventListener('resize', this.__handleWindowResize); } } } /** @private */ __createContainer(slot) { const container = document.createElement('vaadin-dialog-content'); container.setAttribute('slot', slot); return container; } /** @private */ __clearContainer(container) { container.innerHTML = ''; // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into. // When clearing the rendered content, this part needs to be manually disposed of. // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward. delete container._$litPart$; } /** @private */ __initContainer(container, slot) { if (container) { // Reset existing container in case if a new renderer is set. this.__clearContainer(container); } else { // Create the container and append it to the dialog element. container = this.__createContainer(slot); this.owner.appendChild(container); } return container; } /** @private */ _headerFooterRendererChange(headerRenderer, footerRenderer, opened) { const headerRendererChanged = this.__oldHeaderRenderer !== headerRenderer; this.__oldHeaderRenderer = headerRenderer; const footerRendererChanged = this.__oldFooterRenderer !== footerRenderer; this.__oldFooterRenderer = footerRenderer; const openedChanged = this._oldOpenedFooterHeader !== opened; this._oldOpenedFooterHeader = opened; if (headerRendererChanged) { if (headerRenderer) { this.headerContainer = this.__initContainer(this.headerContainer, 'header-content'); } else if (this.headerContainer) { this.headerContainer.remove(); this.headerContainer = null; } } if (footerRendererChanged) { if (footerRenderer) { this.footerContainer = this.__initContainer(this.footerContainer, 'footer'); } else if (this.footerContainer) { this.footerContainer.remove(); this.footerContainer = null; } } if ( (headerRenderer && (headerRendererChanged || openedChanged)) || (footerRenderer && (footerRendererChanged || openedChanged)) ) { if (opened) { this.requestContentUpdate(); } } } /** @private */ _headerTitleChanged(headerTitle, opened) { setOverlayStateAttribute(this, 'has-title', !!headerTitle); if (opened && (headerTitle || this._oldHeaderTitle)) { this.requestContentUpdate(); } this._oldHeaderTitle = headerTitle; } /** @private */ _headerTitleRenderer() { if (this.headerTitle) { if (!this.headerTitleElement) { this.headerTitleElement = document.createElement('h2'); this.headerTitleElement.setAttribute('slot', 'title'); this.headerTitleElement.classList.add('draggable'); } this.owner.appendChild(this.headerTitleElement); this.headerTitleElement.textContent = this.headerTitle; } else if (this.headerTitleElement) { this.headerTitleElement.remove(); this.headerTitleElement = null; } } /** * @override */ requestContentUpdate() { super.requestContentUpdate(); if (this.headerContainer && this.headerRenderer) { this.headerRenderer.call(this.owner, this.headerContainer, this.owner); } if (this.footerContainer && this.footerRenderer) { this.footerRenderer.call(this.owner, this.footerContainer, this.owner); } this._headerTitleRenderer(); this.__updateOverflow(); } /** * Retrieves the coordinates of the overlay. * @return {!DialogOverlayBounds} */ getBounds() { const overlayBounds = this.$.overlay.getBoundingClientRect(); const containerBounds = this.getBoundingClientRect(); const top = overlayBounds.top - containerBounds.top; const left = overlayBounds.left - containerBounds.left; const width = overlayBounds.width; const height = overlayBounds.height; return { top, left, width, height }; } /** * Override method from OverlayMixin to adjust the position of the overlay if `keepInViewport` is true. * @override */ setBounds(bounds, absolute = true) { super.setBounds(bounds, absolute); this.__adjustPosition(); } /** @private */ __updateOverflow() { let overflow = ''; const content = this.$.content; if (content.scrollTop > 0) { overflow += ' top'; } if (content.scrollTop < content.scrollHeight - content.clientHeight) { overflow += ' bottom'; } const value = overflow.trim(); if (value.length > 0 && this.getAttribute('overflow') !== value) { setOverlayStateAttribute(this, 'overflow', value); } else if (value.length === 0 && this.hasAttribute('overflow')) { setOverlayStateAttribute(this, 'overflow', null); } } /** @private */ __handleWindowResize() { this.__adjustPosition(); } /** * Adjusts the position of the overlay to keep it within the viewport if `keepInViewport` is true. * @private */ __adjustPosition() { if (!this.opened || !this.keepInViewport) { return; } // Centered dialogs do not use absolute positioning and automatically adjust their position / size to fit the viewport const style = getComputedStyle(this.$.overlay); if (style.position !== 'absolute') { return; } const overlayHostBounds = this.getBoundingClientRect(); const bounds = this.getBounds(); // Prefer dimensions from getComputedStyle, as bounding rect is affected // by scale transform applied by opening animation in Lumo const width = parseFloat(style.width) || bounds.width; const height = parseFloat(style.height) || bounds.height; const maxLeft = overlayHostBounds.right - overlayHostBounds.left - width; const maxTop = overlayHostBounds.bottom - overlayHostBounds.top - height; if (bounds.left > maxLeft || bounds.top > maxTop) { const left = Math.max(0, Math.min(bounds.left, maxLeft)); const top = Math.max(0, Math.min(bounds.top, maxTop)); Object.assign(this.$.overlay.style, { left: `${left}px`, top: `${top}px`, }); } } };