notificationpackage.src.vaadin-notification.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaadin-webcomponents Show documentation
Show all versions of vaadin-webcomponents Show documentation
Mvnpm composite: Vaadin webcomponents
The newest version!
/**
* @license
* Copyright (c) 2017 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
import { render } from 'lit';
import { isTemplateResult } from 'lit/directive-helpers.js';
import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
import { processTemplates } from '@vaadin/component-base/src/templates.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
/**
* An element used internally by ``. Not intended to be used separately.
*
* @customElement
* @extends HTMLElement
* @mixes ElementMixin
* @mixes ThemableMixin
* @private
*/
class NotificationContainer extends ThemableMixin(ElementMixin(PolymerElement)) {
static get template() {
return html`
`;
}
static get is() {
return 'vaadin-notification-container';
}
static get properties() {
return {
/**
* True when the container is opened
* @type {boolean}
*/
opened: {
type: Boolean,
value: false,
observer: '_openedChanged',
},
};
}
constructor() {
super();
this._boundVaadinOverlayClose = this._onVaadinOverlayClose.bind(this);
if (isIOS) {
this._boundIosResizeListener = () => this._detectIosNavbar();
}
}
/** @private */
_openedChanged(opened) {
if (opened) {
document.body.appendChild(this);
document.addEventListener('vaadin-overlay-close', this._boundVaadinOverlayClose);
if (this._boundIosResizeListener) {
this._detectIosNavbar();
window.addEventListener('resize', this._boundIosResizeListener);
}
} else {
document.body.removeChild(this);
document.removeEventListener('vaadin-overlay-close', this._boundVaadinOverlayClose);
if (this._boundIosResizeListener) {
window.removeEventListener('resize', this._boundIosResizeListener);
}
}
}
/** @private */
_detectIosNavbar() {
const innerHeight = window.innerHeight;
const innerWidth = window.innerWidth;
const landscape = innerWidth > innerHeight;
const clientHeight = document.documentElement.clientHeight;
if (landscape && clientHeight > innerHeight) {
this.style.bottom = `${clientHeight - innerHeight}px`;
} else {
this.style.bottom = '0';
}
}
/** @private */
_onVaadinOverlayClose(event) {
// Notifications are a separate overlay mechanism from vaadin-overlay, and
// interacting with them should not close modal overlays
const sourceEvent = event.detail.sourceEvent;
const isFromNotification = sourceEvent && sourceEvent.composedPath().indexOf(this) >= 0;
if (isFromNotification) {
event.preventDefault();
}
}
}
/**
* An element used internally by ``. Not intended to be used separately.
*
* @customElement
* @extends HTMLElement
* @mixes ThemableMixin
* @private
*/
class NotificationCard extends ThemableMixin(PolymerElement) {
static get template() {
return html`
`;
}
static get is() {
return 'vaadin-notification-card';
}
/** @protected */
ready() {
super.ready();
this.setAttribute('role', 'alert');
this.setAttribute('aria-live', 'polite');
}
}
/**
* `` is a Web Component providing accessible and customizable notifications (toasts).
*
* ### Rendering
*
* The content of the notification can be populated by using the renderer callback function.
*
* The renderer function provides `root`, `notification` arguments.
* Generate DOM content, append it to the `root` element and control the state
* of the host element by accessing `notification`. Before generating new content,
* users are able to check if there is already content in `root` for reusing it.
*
* ```html
*
* ```
* ```js
* const notification = document.querySelector('#notification');
* notification.renderer = function(root, notification) {
* root.textContent = "Your work has been saved";
* };
* ```
*
* Renderer is called on the opening of the notification.
* DOM generated during the renderer call can be reused
* in the next renderer call and will be provided with the `root` argument.
* On first call it will be empty.
*
* ### Styling
*
* `` uses `` internal
* themable component as the actual visible notification cards.
*
* The following shadow DOM parts of the `` are available for styling:
*
* Part name | Description
* ----------------|----------------
* `overlay` | The notification container
* `content` | The content of the notification
*
* See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
*
* Note: the `theme` attribute value set on `` is
* propagated to the internal ``.
*
* @fires {CustomEvent} opened-changed - Fired when the `opened` property changes.
* @fires {CustomEvent} closed - Fired when the notification is closed.
*
* @customElement
* @extends HTMLElement
* @mixes ThemePropertyMixin
* @mixes ElementMixin
* @mixes OverlayClassMixin
*/
class Notification extends OverlayClassMixin(ThemePropertyMixin(ElementMixin(PolymerElement))) {
static get template() {
return html`
`;
}
static get is() {
return 'vaadin-notification';
}
static get properties() {
return {
/**
* The duration in milliseconds to show the notification.
* Set to `0` or a negative number to disable the notification auto-closing.
* @type {number}
*/
duration: {
type: Number,
value: 5000,
},
/**
* True if the notification is currently displayed.
* @type {boolean}
*/
opened: {
type: Boolean,
value: false,
notify: true,
observer: '_openedChanged',
},
/**
* Alignment of the notification in the viewport
* Valid values are `top-stretch|top-start|top-center|top-end|middle|bottom-start|bottom-center|bottom-end|bottom-stretch`
* @type {!NotificationPosition}
*/
position: {
type: String,
value: 'bottom-start',
observer: '_positionChanged',
},
/**
* Custom function for rendering the content of the notification.
* Receives two arguments:
*
* - `root` The `` DOM element. Append
* your content to it.
* - `notification` The reference to the `` element.
* @type {!NotificationRenderer | undefined}
*/
renderer: Function,
};
}
static get observers() {
return ['_durationChanged(duration, opened)', '_rendererChanged(renderer, opened, _overlayElement)'];
}
/**
* Shows a notification with the given content.
* By default, positions the notification at `bottom-start` and uses a 5 second duration.
* An options object can be passed to configure the notification.
* The options object has the following structure:
*
* ```
* {
* position?: string
* duration?: number
* theme?: string
* }
* ```
*
* See the individual documentation for:
* - [`position`](#/elements/vaadin-notification#property-position)
* - [`duration`](#/elements/vaadin-notification#property-duration)
*
* @param contents the contents to show, either as a string or a Lit template.
* @param options optional options for customizing the notification.
*/
static show(contents, options) {
if (isTemplateResult(contents)) {
return Notification._createAndShowNotification((root) => {
render(contents, root);
}, options);
}
return Notification._createAndShowNotification((root) => {
root.innerText = contents;
}, options);
}
/** @private */
static _createAndShowNotification(renderer, options) {
const notification = document.createElement(Notification.is);
if (options && Number.isFinite(options.duration)) {
notification.duration = options.duration;
}
if (options && options.position) {
notification.position = options.position;
}
if (options && options.theme) {
notification.setAttribute('theme', options.theme);
}
notification.renderer = renderer;
document.body.appendChild(notification);
notification.opened = true;
notification.addEventListener('opened-changed', (e) => {
if (!e.detail.value) {
notification.remove();
}
});
return notification;
}
/** @private */
get _container() {
if (!Notification._container) {
Notification._container = document.createElement('vaadin-notification-container');
document.body.appendChild(Notification._container);
}
return Notification._container;
}
/** @protected */
get _card() {
return this._overlayElement;
}
/** @protected */
ready() {
super.ready();
this._overlayElement = this.shadowRoot.querySelector('vaadin-notification-card');
processTemplates(this);
}
/** @protected */
disconnectedCallback() {
super.disconnectedCallback();
queueMicrotask(() => {
if (!this.isConnected) {
this.opened = false;
}
});
}
/**
* Requests an update for the content of the notification.
* While performing the update, it invokes the renderer passed in the `renderer` property.
*
* It is not guaranteed that the update happens immediately (synchronously) after it is requested.
*/
requestContentUpdate() {
if (!this.renderer) {
return;
}
this.renderer(this._card, this);
}
/** @private */
_rendererChanged(renderer, opened, card) {
if (!card) {
return;
}
const rendererChanged = this._oldRenderer !== renderer;
this._oldRenderer = renderer;
if (rendererChanged) {
card.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 card._$litPart$;
}
if (opened) {
if (!this._didAnimateNotificationAppend) {
this._animatedAppendNotificationCard();
}
this.requestContentUpdate();
}
}
/**
* Opens the notification.
*/
open() {
this.opened = true;
}
/**
* Closes the notification.
*/
close() {
this.opened = false;
}
/** @private */
_openedChanged(opened) {
if (opened) {
this._container.opened = true;
this._animatedAppendNotificationCard();
} else if (this._card) {
this._closeNotificationCard();
}
}
/** @private */
__cleanUpOpeningClosingState() {
this._card.removeAttribute('opening');
this._card.removeAttribute('closing');
this._card.removeEventListener('animationend', this.__animationEndListener);
}
/** @private */
_animatedAppendNotificationCard() {
if (this._card) {
this.__cleanUpOpeningClosingState();
this._card.setAttribute('opening', '');
this._appendNotificationCard();
this.__animationEndListener = () => this.__cleanUpOpeningClosingState();
this._card.addEventListener('animationend', this.__animationEndListener);
this._didAnimateNotificationAppend = true;
} else {
this._didAnimateNotificationAppend = false;
}
}
/** @private */
_appendNotificationCard() {
if (!this._card) {
return;
}
if (!this._container.shadowRoot.querySelector(`slot[name="${this.position}"]`)) {
console.warn(`Invalid alignment parameter provided: position=${this.position}`);
return;
}
this._card.slot = this.position;
if (this._container.firstElementChild && /top/u.test(this.position)) {
this._container.insertBefore(this._card, this._container.firstElementChild);
} else {
this._container.appendChild(this._card);
}
}
/** @private */
_removeNotificationCard() {
if (this._card.parentNode) {
this._card.parentNode.removeChild(this._card);
}
this._card.removeAttribute('closing');
this._container.opened = Boolean(this._container.firstElementChild);
this.dispatchEvent(new CustomEvent('closed'));
}
/** @private */
_closeNotificationCard() {
if (this._durationTimeoutId) {
clearTimeout(this._durationTimeoutId);
}
this._animatedRemoveNotificationCard();
}
/** @private */
_animatedRemoveNotificationCard() {
this.__cleanUpOpeningClosingState();
this._card.setAttribute('closing', '');
const name = getComputedStyle(this._card).getPropertyValue('animation-name');
if (name && name !== 'none') {
this.__animationEndListener = () => {
this._removeNotificationCard();
this.__cleanUpOpeningClosingState();
};
this._card.addEventListener('animationend', this.__animationEndListener);
} else {
this._removeNotificationCard();
}
}
/** @private */
_positionChanged() {
if (this.opened) {
this._animatedAppendNotificationCard();
}
}
/** @private */
_durationChanged(duration, opened) {
if (opened) {
clearTimeout(this._durationTimeoutId);
if (duration > 0) {
this._durationTimeoutId = setTimeout(() => this.close(), duration);
}
}
}
/**
* Fired when the notification is closed.
*
* @event closed
*/
}
defineCustomElement(NotificationContainer);
defineCustomElement(NotificationCard);
defineCustomElement(Notification);
export { Notification };