@vaadin/notification 24.6.0-alpha4 → 24.6.0-alpha6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/notification",
3
- "version": "24.6.0-alpha4",
3
+ "version": "24.6.0-alpha6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -37,17 +37,18 @@
37
37
  "polymer"
38
38
  ],
39
39
  "dependencies": {
40
+ "@open-wc/dedupe-mixin": "^1.3.0",
40
41
  "@polymer/polymer": "^3.0.0",
41
- "@vaadin/component-base": "24.6.0-alpha4",
42
- "@vaadin/lit-renderer": "24.6.0-alpha4",
43
- "@vaadin/vaadin-lumo-styles": "24.6.0-alpha4",
44
- "@vaadin/vaadin-material-styles": "24.6.0-alpha4",
45
- "@vaadin/vaadin-themable-mixin": "24.6.0-alpha4",
42
+ "@vaadin/component-base": "24.6.0-alpha6",
43
+ "@vaadin/lit-renderer": "24.6.0-alpha6",
44
+ "@vaadin/vaadin-lumo-styles": "24.6.0-alpha6",
45
+ "@vaadin/vaadin-material-styles": "24.6.0-alpha6",
46
+ "@vaadin/vaadin-themable-mixin": "24.6.0-alpha6",
46
47
  "lit": "^3.0.0"
47
48
  },
48
49
  "devDependencies": {
49
- "@vaadin/button": "24.6.0-alpha4",
50
- "@vaadin/chai-plugins": "24.6.0-alpha4",
50
+ "@vaadin/button": "24.6.0-alpha6",
51
+ "@vaadin/chai-plugins": "24.6.0-alpha6",
51
52
  "@vaadin/testing-helpers": "^1.0.0",
52
53
  "sinon": "^18.0.0"
53
54
  },
@@ -55,5 +56,5 @@
55
56
  "web-types.json",
56
57
  "web-types.lit.json"
57
58
  ],
58
- "gitHead": "78967d4f3bb46f58f43c2cc621802554acb2efaf"
59
+ "gitHead": "53f2f0c6cb9324a832ee7c0a052db927e151e167"
59
60
  }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { css, html, LitElement } from 'lit';
7
+ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
8
+ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
9
+ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
10
+ import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11
+ import { NotificationContainerMixin, NotificationMixin } from './vaadin-notification-mixin.js';
12
+ import { notificationCardStyles, notificationContainerStyles } from './vaadin-notification-styles.js';
13
+
14
+ /**
15
+ * An element used internally by `<vaadin-notification>`. Not intended to be used separately.
16
+ *
17
+ * @customElement
18
+ * @extends HTMLElement
19
+ * @mixes NotificationContainerMixin
20
+ * @mixes ElementMixin
21
+ * @mixes ThemableMixin
22
+ * @private
23
+ */
24
+ class NotificationContainer extends NotificationContainerMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
25
+ static get styles() {
26
+ return notificationContainerStyles;
27
+ }
28
+
29
+ render() {
30
+ return html`
31
+ <div region="top-stretch"><slot name="top-stretch"></slot></div>
32
+ <div region-group="top">
33
+ <div region="top-start"><slot name="top-start"></slot></div>
34
+ <div region="top-center"><slot name="top-center"></slot></div>
35
+ <div region="top-end"><slot name="top-end"></slot></div>
36
+ </div>
37
+ <div region="middle"><slot name="middle"></slot></div>
38
+ <div region-group="bottom">
39
+ <div region="bottom-start"><slot name="bottom-start"></slot></div>
40
+ <div region="bottom-center"><slot name="bottom-center"></slot></div>
41
+ <div region="bottom-end"><slot name="bottom-end"></slot></div>
42
+ </div>
43
+ <div region="bottom-stretch"><slot name="bottom-stretch"></slot></div>
44
+ `;
45
+ }
46
+
47
+ static get is() {
48
+ return 'vaadin-notification-container';
49
+ }
50
+ }
51
+
52
+ /**
53
+ * An element used internally by `<vaadin-notification>`. Not intended to be used separately.
54
+ *
55
+ * @customElement
56
+ * @extends HTMLElement
57
+ * @mixes ThemableMixin
58
+ * @mixes ElementMixin
59
+ * @private
60
+ */
61
+ class NotificationCard extends ElementMixin(ThemableMixin(PolylitMixin(LitElement))) {
62
+ static get styles() {
63
+ return notificationCardStyles;
64
+ }
65
+
66
+ render() {
67
+ return html`
68
+ <div part="overlay">
69
+ <div part="content">
70
+ <slot></slot>
71
+ </div>
72
+ </div>
73
+ `;
74
+ }
75
+
76
+ static get is() {
77
+ return 'vaadin-notification-card';
78
+ }
79
+
80
+ /** @protected */
81
+ ready() {
82
+ super.ready();
83
+ this.setAttribute('role', 'alert');
84
+ }
85
+ }
86
+
87
+ /**
88
+ * LitElement based version of `<vaadin-notification>` web component.
89
+ *
90
+ * ## Disclaimer
91
+ *
92
+ * This component is an experiment and not yet a part of Vaadin platform.
93
+ * There is no ETA regarding specific Vaadin version where it'll land.
94
+ * Feel free to try this code in your apps as per Apache 2.0 license.
95
+ */
96
+ class Notification extends NotificationMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
97
+ static get styles() {
98
+ return css`
99
+ :host {
100
+ display: none !important;
101
+ }
102
+ `;
103
+ }
104
+
105
+ render() {
106
+ return html`
107
+ <vaadin-notification-card
108
+ theme="${this._theme || ''}"
109
+ aria-live="${this.__computeAriaLive(this.assertive)}"
110
+ ></vaadin-notification-card>
111
+ `;
112
+ }
113
+
114
+ static get is() {
115
+ return 'vaadin-notification';
116
+ }
117
+
118
+ /**
119
+ * Fired when the notification is closed.
120
+ *
121
+ * @event closed
122
+ */
123
+ }
124
+
125
+ defineCustomElement(NotificationContainer);
126
+ defineCustomElement(NotificationCard);
127
+ defineCustomElement(Notification);
128
+
129
+ export { Notification };
@@ -0,0 +1,92 @@
1
+ import type { Constructor } from '@open-wc/dedupe-mixin';
2
+ import type { OverlayClassMixinClass } from '@vaadin/component-base/src/overlay-class-mixin';
3
+ import type { ThemePropertyMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin';
4
+ import type { Notification } from './vaadin-notification.js';
5
+
6
+ export type NotificationPosition =
7
+ | 'bottom-center'
8
+ | 'bottom-end'
9
+ | 'bottom-start'
10
+ | 'bottom-stretch'
11
+ | 'middle'
12
+ | 'top-center'
13
+ | 'top-end'
14
+ | 'top-start'
15
+ | 'top-stretch';
16
+
17
+ export type NotificationRenderer = (root: HTMLElement, notification: Notification) => void;
18
+
19
+ /**
20
+ * A mixin providing common notification container functionality.
21
+ */
22
+ export declare function NotificationContainerMixin<T extends Constructor<HTMLElement>>(
23
+ base: T,
24
+ ): Constructor<NotificationContainerMixinClass> & T;
25
+
26
+ export declare class NotificationContainerMixinClass {
27
+ /**
28
+ * True when the container is opened
29
+ */
30
+ opened: boolean;
31
+ }
32
+
33
+ /**
34
+ * A mixin providing common notification functionality.
35
+ */
36
+ export declare function NotificationMixin<T extends Constructor<HTMLElement>>(
37
+ base: T,
38
+ ): Constructor<NotificationMixinClass> & Constructor<ThemePropertyMixinClass> & Constructor<OverlayClassMixinClass> & T;
39
+
40
+ export declare class NotificationMixinClass {
41
+ /**
42
+ * When true, the notification card has `aria-live` attribute set to
43
+ * `assertive` instead of `polite`. This makes screen readers announce
44
+ * the notification content immediately when it appears.
45
+ */
46
+ assertive: boolean;
47
+
48
+ /**
49
+ * The duration in milliseconds to show the notification.
50
+ * Set to `0` or a negative number to disable the notification auto-closing.
51
+ */
52
+ duration: number;
53
+
54
+ /**
55
+ * True if the notification is currently displayed.
56
+ */
57
+ opened: boolean;
58
+
59
+ /**
60
+ * Alignment of the notification in the viewport
61
+ * Valid values are `top-stretch|top-start|top-center|top-end|middle|bottom-start|bottom-center|bottom-end|bottom-stretch`
62
+ */
63
+ position: NotificationPosition;
64
+
65
+ /**
66
+ * Custom function for rendering the content of the notification.
67
+ * Receives two arguments:
68
+ *
69
+ * - `root` The `<vaadin-notification-card>` DOM element. Append
70
+ * your content to it.
71
+ * - `notification` The reference to the `<vaadin-notification>` element.
72
+ */
73
+ renderer: NotificationRenderer | undefined;
74
+
75
+ /**
76
+ * Requests an update for the content of the notification.
77
+ * While performing the update, it invokes the renderer passed in the `renderer` property.
78
+ *
79
+ * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
80
+ */
81
+ requestContentUpdate(): void;
82
+
83
+ /**
84
+ * Opens the notification.
85
+ */
86
+ open(): void;
87
+
88
+ /**
89
+ * Closes the notification.
90
+ */
91
+ close(): void;
92
+ }
@@ -0,0 +1,434 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2017 - 2024 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { render } from 'lit';
7
+ import { isTemplateResult } from 'lit/directive-helpers.js';
8
+ import { isIOS } from '@vaadin/component-base/src/browser-utils.js';
9
+ import { OverlayClassMixin } from '@vaadin/component-base/src/overlay-class-mixin.js';
10
+ import { processTemplates } from '@vaadin/component-base/src/templates.js';
11
+ import { ThemePropertyMixin } from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js';
12
+
13
+ /**
14
+ * A mixin providing common notification container functionality.
15
+ *
16
+ * @polymerMixin
17
+ */
18
+ export const NotificationContainerMixin = (superClass) =>
19
+ class extends superClass {
20
+ static get properties() {
21
+ return {
22
+ /**
23
+ * True when the container is opened
24
+ * @type {boolean}
25
+ */
26
+ opened: {
27
+ type: Boolean,
28
+ value: false,
29
+ sync: true,
30
+ observer: '_openedChanged',
31
+ },
32
+ };
33
+ }
34
+
35
+ constructor() {
36
+ super();
37
+
38
+ this._boundVaadinOverlayClose = this._onVaadinOverlayClose.bind(this);
39
+ if (isIOS) {
40
+ this._boundIosResizeListener = () => this._detectIosNavbar();
41
+ }
42
+ }
43
+
44
+ /** @private */
45
+ _openedChanged(opened) {
46
+ if (opened) {
47
+ document.body.appendChild(this);
48
+ document.addEventListener('vaadin-overlay-close', this._boundVaadinOverlayClose);
49
+ if (this._boundIosResizeListener) {
50
+ this._detectIosNavbar();
51
+ window.addEventListener('resize', this._boundIosResizeListener);
52
+ }
53
+ } else {
54
+ document.body.removeChild(this);
55
+ document.removeEventListener('vaadin-overlay-close', this._boundVaadinOverlayClose);
56
+ if (this._boundIosResizeListener) {
57
+ window.removeEventListener('resize', this._boundIosResizeListener);
58
+ }
59
+ }
60
+ }
61
+
62
+ /** @private */
63
+ _detectIosNavbar() {
64
+ const innerHeight = window.innerHeight;
65
+ const innerWidth = window.innerWidth;
66
+ const landscape = innerWidth > innerHeight;
67
+ const clientHeight = document.documentElement.clientHeight;
68
+ if (landscape && clientHeight > innerHeight) {
69
+ this.style.bottom = `${clientHeight - innerHeight}px`;
70
+ } else {
71
+ this.style.bottom = '0';
72
+ }
73
+ }
74
+
75
+ /** @private */
76
+ _onVaadinOverlayClose(event) {
77
+ // Notifications are a separate overlay mechanism from vaadin-overlay, and
78
+ // interacting with them should not close modal overlays
79
+ const sourceEvent = event.detail.sourceEvent;
80
+ const isFromNotification = sourceEvent && sourceEvent.composedPath().indexOf(this) >= 0;
81
+ if (isFromNotification) {
82
+ event.preventDefault();
83
+ }
84
+ }
85
+ };
86
+
87
+ /**
88
+ * A mixin providing common notification functionality.
89
+ *
90
+ * @polymerMixin
91
+ * @mixes OverlayClassMixin
92
+ * @mixes ThemePropertyMixin
93
+ */
94
+ export const NotificationMixin = (superClass) =>
95
+ class extends ThemePropertyMixin(OverlayClassMixin(superClass)) {
96
+ static get properties() {
97
+ return {
98
+ /**
99
+ * When true, the notification card has `aria-live` attribute set to
100
+ * `assertive` instead of `polite`. This makes screen readers announce
101
+ * the notification content immediately when it appears.
102
+ */
103
+ assertive: {
104
+ type: Boolean,
105
+ value: false,
106
+ sync: true,
107
+ },
108
+
109
+ /**
110
+ * The duration in milliseconds to show the notification.
111
+ * Set to `0` or a negative number to disable the notification auto-closing.
112
+ * @type {number}
113
+ */
114
+ duration: {
115
+ type: Number,
116
+ value: 5000,
117
+ sync: true,
118
+ },
119
+
120
+ /**
121
+ * True if the notification is currently displayed.
122
+ * @type {boolean}
123
+ */
124
+ opened: {
125
+ type: Boolean,
126
+ value: false,
127
+ notify: true,
128
+ sync: true,
129
+ observer: '_openedChanged',
130
+ },
131
+
132
+ /**
133
+ * Alignment of the notification in the viewport
134
+ * Valid values are `top-stretch|top-start|top-center|top-end|middle|bottom-start|bottom-center|bottom-end|bottom-stretch`
135
+ * @type {!NotificationPosition}
136
+ */
137
+ position: {
138
+ type: String,
139
+ value: 'bottom-start',
140
+ observer: '_positionChanged',
141
+ sync: true,
142
+ },
143
+
144
+ /**
145
+ * Custom function for rendering the content of the notification.
146
+ * Receives two arguments:
147
+ *
148
+ * - `root` The `<vaadin-notification-card>` DOM element. Append
149
+ * your content to it.
150
+ * - `notification` The reference to the `<vaadin-notification>` element.
151
+ * @type {!NotificationRenderer | undefined}
152
+ */
153
+ renderer: {
154
+ type: Function,
155
+ sync: true,
156
+ },
157
+ };
158
+ }
159
+
160
+ static get observers() {
161
+ return ['_durationChanged(duration, opened)', '_rendererChanged(renderer, opened, _overlayElement)'];
162
+ }
163
+
164
+ /**
165
+ * Shows a notification with the given content.
166
+ * By default, positions the notification at `bottom-start` and uses a 5 second duration.
167
+ * An options object can be passed to configure the notification.
168
+ * The options object has the following structure:
169
+ *
170
+ * ```
171
+ * {
172
+ * assertive?: boolean
173
+ * position?: string
174
+ * duration?: number
175
+ * theme?: string
176
+ * }
177
+ * ```
178
+ *
179
+ * See the individual documentation for:
180
+ * - [`assertive`](#/elements/vaadin-notification#property-assertive)
181
+ * - [`position`](#/elements/vaadin-notification#property-position)
182
+ * - [`duration`](#/elements/vaadin-notification#property-duration)
183
+ *
184
+ * @param contents the contents to show, either as a string or a Lit template.
185
+ * @param options optional options for customizing the notification.
186
+ */
187
+ static show(contents, options) {
188
+ const Notification = customElements.get('vaadin-notification');
189
+ if (isTemplateResult(contents)) {
190
+ return Notification._createAndShowNotification((root) => {
191
+ render(contents, root);
192
+ }, options);
193
+ }
194
+ return Notification._createAndShowNotification((root) => {
195
+ root.innerText = contents;
196
+ }, options);
197
+ }
198
+
199
+ /** @private */
200
+ static _createAndShowNotification(renderer, options) {
201
+ const notification = document.createElement('vaadin-notification');
202
+ if (options && Number.isFinite(options.duration)) {
203
+ notification.duration = options.duration;
204
+ }
205
+ if (options && options.position) {
206
+ notification.position = options.position;
207
+ }
208
+ if (options && options.assertive) {
209
+ notification.assertive = options.assertive;
210
+ }
211
+ if (options && options.theme) {
212
+ notification.setAttribute('theme', options.theme);
213
+ }
214
+ notification.renderer = renderer;
215
+ document.body.appendChild(notification);
216
+ notification.opened = true;
217
+
218
+ notification.addEventListener('opened-changed', (e) => {
219
+ if (!e.detail.value) {
220
+ notification.remove();
221
+ }
222
+ });
223
+
224
+ return notification;
225
+ }
226
+
227
+ /** @private */
228
+ get _container() {
229
+ if (!Notification._container) {
230
+ Notification._container = document.createElement('vaadin-notification-container');
231
+ document.body.appendChild(Notification._container);
232
+ }
233
+ return Notification._container;
234
+ }
235
+
236
+ /** @protected */
237
+ get _card() {
238
+ return this._overlayElement;
239
+ }
240
+
241
+ /** @protected */
242
+ ready() {
243
+ super.ready();
244
+
245
+ this._overlayElement = this.shadowRoot.querySelector('vaadin-notification-card');
246
+
247
+ processTemplates(this);
248
+ }
249
+
250
+ /** @protected */
251
+ disconnectedCallback() {
252
+ super.disconnectedCallback();
253
+ queueMicrotask(() => {
254
+ if (!this.isConnected) {
255
+ this.opened = false;
256
+ }
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Requests an update for the content of the notification.
262
+ * While performing the update, it invokes the renderer passed in the `renderer` property.
263
+ *
264
+ * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
265
+ */
266
+ requestContentUpdate() {
267
+ if (!this.renderer) {
268
+ return;
269
+ }
270
+
271
+ this.renderer(this._card, this);
272
+ }
273
+
274
+ /** @private */
275
+ __computeAriaLive(assertive) {
276
+ return assertive ? 'assertive' : 'polite';
277
+ }
278
+
279
+ /** @private */
280
+ _rendererChanged(renderer, opened, card) {
281
+ if (!card) {
282
+ return;
283
+ }
284
+
285
+ const rendererChanged = this._oldRenderer !== renderer;
286
+ this._oldRenderer = renderer;
287
+
288
+ if (rendererChanged) {
289
+ card.innerHTML = '';
290
+ // Whenever a Lit-based renderer is used, it assigns a Lit part to the node it was rendered into.
291
+ // When clearing the rendered content, this part needs to be manually disposed of.
292
+ // Otherwise, using a Lit-based renderer on the same node will throw an exception or render nothing afterward.
293
+ delete card._$litPart$;
294
+ }
295
+
296
+ if (opened) {
297
+ if (!this._didAnimateNotificationAppend) {
298
+ this._animatedAppendNotificationCard();
299
+ }
300
+ this.requestContentUpdate();
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Opens the notification.
306
+ */
307
+ open() {
308
+ this.opened = true;
309
+ }
310
+
311
+ /**
312
+ * Closes the notification.
313
+ */
314
+ close() {
315
+ this.opened = false;
316
+ }
317
+
318
+ /** @private */
319
+ _openedChanged(opened) {
320
+ if (opened) {
321
+ this._container.opened = true;
322
+ this._animatedAppendNotificationCard();
323
+ } else if (this._card) {
324
+ this._closeNotificationCard();
325
+ }
326
+ }
327
+
328
+ /** @private */
329
+ __cleanUpOpeningClosingState() {
330
+ this._card.removeAttribute('opening');
331
+ this._card.removeAttribute('closing');
332
+ this._card.removeEventListener('animationend', this.__animationEndListener);
333
+ }
334
+
335
+ /** @private */
336
+ _animatedAppendNotificationCard() {
337
+ if (this._card) {
338
+ this.__cleanUpOpeningClosingState();
339
+
340
+ this._card.setAttribute('opening', '');
341
+ this._appendNotificationCard();
342
+ this.__animationEndListener = () => this.__cleanUpOpeningClosingState();
343
+ this._card.addEventListener('animationend', this.__animationEndListener);
344
+ this._didAnimateNotificationAppend = true;
345
+ } else {
346
+ this._didAnimateNotificationAppend = false;
347
+ }
348
+ }
349
+
350
+ /** @private */
351
+ _appendNotificationCard() {
352
+ if (!this._card) {
353
+ return;
354
+ }
355
+
356
+ if (this._container.performUpdate) {
357
+ this._container.performUpdate();
358
+ }
359
+
360
+ if (!this._container.shadowRoot.querySelector(`slot[name="${this.position}"]`)) {
361
+ console.warn(`Invalid alignment parameter provided: position=${this.position}`);
362
+ return;
363
+ }
364
+
365
+ this._card.slot = this.position;
366
+ if (this._container.firstElementChild && /top/u.test(this.position)) {
367
+ this._container.insertBefore(this._card, this._container.firstElementChild);
368
+ } else {
369
+ this._container.appendChild(this._card);
370
+ }
371
+ }
372
+
373
+ /** @private */
374
+ _removeNotificationCard() {
375
+ if (!this._card) {
376
+ return;
377
+ }
378
+
379
+ if (this._card.parentNode) {
380
+ this._card.parentNode.removeChild(this._card);
381
+ }
382
+ this._card.removeAttribute('closing');
383
+ this._container.opened = Boolean(this._container.firstElementChild);
384
+ this.dispatchEvent(new CustomEvent('closed'));
385
+ }
386
+
387
+ /** @private */
388
+ _closeNotificationCard() {
389
+ if (this._durationTimeoutId) {
390
+ clearTimeout(this._durationTimeoutId);
391
+ }
392
+ this._animatedRemoveNotificationCard();
393
+ }
394
+
395
+ /** @private */
396
+ _animatedRemoveNotificationCard() {
397
+ this.__cleanUpOpeningClosingState();
398
+
399
+ this._card.setAttribute('closing', '');
400
+ const name = getComputedStyle(this._card).getPropertyValue('animation-name');
401
+ if (name && name !== 'none') {
402
+ this.__animationEndListener = () => {
403
+ this._removeNotificationCard();
404
+ this.__cleanUpOpeningClosingState();
405
+ };
406
+ this._card.addEventListener('animationend', this.__animationEndListener);
407
+ } else {
408
+ this._removeNotificationCard();
409
+ }
410
+ }
411
+
412
+ /** @private */
413
+ _positionChanged() {
414
+ if (this.opened) {
415
+ this._animatedAppendNotificationCard();
416
+ }
417
+ }
418
+
419
+ /** @private */
420
+ _durationChanged(duration, opened) {
421
+ if (opened) {
422
+ clearTimeout(this._durationTimeoutId);
423
+ if (duration > 0) {
424
+ this._durationTimeoutId = setTimeout(() => this.close(), duration);
425
+ }
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Fired when the notification is closed.
431
+ *
432
+ * @event closed
433
+ */
434
+ };