@dile/ui 2.27.0 → 2.27.2

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.
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './button.js';
3
+
4
+ describe('dile-button', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = '';
7
+ });
8
+
9
+ async function renderButton(html) {
10
+ document.body.innerHTML = html;
11
+ const el = document.body.querySelector('dile-button');
12
+ await el.updateComplete;
13
+ return el;
14
+ }
15
+
16
+ it('renders the slotted content inside a native button', async () => {
17
+ const el = await renderButton('<dile-button>Click me</dile-button>');
18
+
19
+ expect(el.shadowRoot.querySelector('button')).toBeTruthy();
20
+ expect(el.textContent.trim()).toBe('Click me');
21
+ });
22
+
23
+ it('reflects the disabled property onto the native button', async () => {
24
+ const el = await renderButton('<dile-button disabled>Click me</dile-button>');
25
+
26
+ expect(el.shadowRoot.querySelector('button').disabled).toBe(true);
27
+ });
28
+
29
+ it('dispatches a click event when enabled', async () => {
30
+ const el = await renderButton('<dile-button>Click me</dile-button>');
31
+ let clicked = false;
32
+ el.addEventListener('click', () => { clicked = true; });
33
+
34
+ el.shadowRoot.querySelector('button').click();
35
+
36
+ expect(clicked).toBe(true);
37
+ });
38
+
39
+ it('shows a spinner icon while loading', async () => {
40
+ const el = await renderButton('<dile-button>Save</dile-button>');
41
+ expect(el.shadowRoot.querySelector('dile-spinner-icon')).toBeNull();
42
+
43
+ el.loading = true;
44
+ await el.updateComplete;
45
+
46
+ expect(el.shadowRoot.querySelector('dile-spinner-icon')).toBeTruthy();
47
+ });
48
+ });
@@ -1,4 +1,5 @@
1
1
  import { LitElement, html, css } from "lit";
2
+ import { repeat } from "lit/directives/repeat.js";
2
3
 
3
4
  export class DileToast extends LitElement {
4
5
  static get properties() {
@@ -7,6 +8,8 @@ export class DileToast extends LitElement {
7
8
  messages: { type: Array },
8
9
  /* Miliseconds the feedback message remains on the screen */
9
10
  duration: { type: Number },
11
+ /* If true, shows a close icon on each toast so the user can dismiss it manually */
12
+ showCloseIcon: { type: Boolean },
10
13
  };
11
14
  }
12
15
 
@@ -15,6 +18,8 @@ export class DileToast extends LitElement {
15
18
  this.messages = [];
16
19
  this.duration = 3000;
17
20
  this.cleanTimeout = false;
21
+ this.showCloseIcon = false;
22
+ this._messageId = 0;
18
23
  }
19
24
 
20
25
  static get styles() {
@@ -31,11 +36,15 @@ export class DileToast extends LitElement {
31
36
 
32
37
  render() {
33
38
  return html`
34
- ${this.messages.map(
39
+ ${repeat(
40
+ this.messages,
41
+ (msg) => msg.id,
35
42
  (msg) => html`
36
43
  <dile-toast-item
37
44
  .msg="${msg}"
45
+ .showCloseIcon="${this.showCloseIcon}"
38
46
  duration="${this.duration}"
47
+ @dile-toast-item-closed="${(e) => this._closeMessage(e.detail.id)}"
39
48
  ></dile-toast-item>
40
49
  `
41
50
  )}
@@ -51,6 +60,7 @@ export class DileToast extends LitElement {
51
60
  this.messages = [
52
61
  ...this.messages,
53
62
  {
63
+ id: ++this._messageId,
54
64
  text,
55
65
  toastType,
56
66
  hidden: false,
@@ -61,6 +71,27 @@ export class DileToast extends LitElement {
61
71
  this._programMessageHide();
62
72
  }
63
73
 
74
+ /**
75
+ * Closes a specific message before its automatic duration elapses, triggered by the user
76
+ */
77
+ _closeMessage(id) {
78
+ const message = this.messages.find((item) => item.id === id);
79
+ if (!message || message.hidden) return;
80
+ this.messages = this.messages.map((item) =>
81
+ item.id === id ? { ...item, hidden: true } : item
82
+ );
83
+ setTimeout(() => {
84
+ this.messages = this.messages.filter((item) => item.id !== id);
85
+ }, 500);
86
+ this.dispatchEvent(
87
+ new CustomEvent("dile-toast-closed-by-user", {
88
+ bubbles: true,
89
+ composed: true,
90
+ detail: { message },
91
+ })
92
+ );
93
+ }
94
+
64
95
  /**
65
96
  * Programs the hiding of the last message, after it's duration time
66
97
  */
@@ -1,9 +1,12 @@
1
1
  import { LitElement, html, css } from "lit";
2
+ import { clearIcon } from "@dile/icons/index.js";
3
+ import "../../icon/icon.js";
2
4
 
3
5
  export class DileToastItem extends LitElement {
4
6
  static get properties() {
5
7
  return {
6
8
  msg: { type: Object },
9
+ showCloseIcon: { type: Boolean },
7
10
  };
8
11
  }
9
12
 
@@ -15,6 +18,7 @@ export class DileToastItem extends LitElement {
15
18
  hidden: false,
16
19
  opening: true,
17
20
  };
21
+ this.showCloseIcon = false;
18
22
  }
19
23
 
20
24
  static get styles() {
@@ -26,6 +30,9 @@ export class DileToastItem extends LitElement {
26
30
  display: block;
27
31
  }
28
32
  div {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
29
36
  color: var(--dile-toast-text-color, #fff);
30
37
  padding: var(--dile-toast-padding, 10px 15px);
31
38
  margin-top: 10px;
@@ -40,6 +47,16 @@ export class DileToastItem extends LitElement {
40
47
  font-weight: var(--dile-toast-font-weight, normal);
41
48
  border-radius: var(--dile-toast-border-radius, 0);
42
49
  }
50
+ .text {
51
+ flex-grow: 1;
52
+ }
53
+ .closeicon {
54
+ cursor: pointer;
55
+ flex-shrink: 0;
56
+ margin-left: 10px;
57
+ --dile-icon-color: var(--dile-toast-close-icon-color, var(--dile-toast-text-color, #fff));
58
+ --dile-icon-size: var(--dile-toast-close-icon-size, 16px);
59
+ }
43
60
  .hidden {
44
61
  top: 20px;
45
62
  opacity: 0;
@@ -68,11 +85,28 @@ export class DileToastItem extends LitElement {
68
85
  ? "opening"
69
86
  : ""} ${this.getClassType(this.msg.toastType)}"
70
87
  >
71
- ${this.msg.text}
88
+ <span class="text">${this.msg.text}</span>
89
+ ${this.showCloseIcon
90
+ ? html`<dile-icon
91
+ .icon="${clearIcon}"
92
+ class="closeicon"
93
+ @click="${this.closeByUser}"
94
+ ></dile-icon>`
95
+ : ""}
72
96
  </div>
73
97
  `;
74
98
  }
75
99
 
100
+ closeByUser() {
101
+ this.dispatchEvent(
102
+ new CustomEvent("dile-toast-item-closed", {
103
+ bubbles: true,
104
+ composed: true,
105
+ detail: { id: this.msg.id, msg: this.msg },
106
+ })
107
+ );
108
+ }
109
+
76
110
  firstUpdated() {
77
111
  setTimeout(() => {
78
112
  this.msg.opening = false;
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import './toast.js';
3
+
4
+ describe('dile-toast', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = '';
7
+ });
8
+
9
+ async function renderToast(html) {
10
+ document.body.innerHTML = html;
11
+ const el = document.body.querySelector('dile-toast');
12
+ await el.updateComplete;
13
+ return el;
14
+ }
15
+
16
+ async function openAndGetItem(el, text, toastType) {
17
+ el.open(text, toastType);
18
+ await el.updateComplete;
19
+ const item = el.shadowRoot.querySelector('dile-toast-item');
20
+ await item.updateComplete;
21
+ return item;
22
+ }
23
+
24
+ function wait(ms) {
25
+ return new Promise((resolve) => setTimeout(resolve, ms));
26
+ }
27
+
28
+ it('does not render a close icon by default', async () => {
29
+ const el = await renderToast('<dile-toast duration="10000"></dile-toast>');
30
+ const item = await openAndGetItem(el, 'Hello');
31
+
32
+ expect(item.shadowRoot.querySelector('.closeicon')).toBeNull();
33
+ });
34
+
35
+ it('renders the message text and status class', async () => {
36
+ const el = await renderToast('<dile-toast duration="10000"></dile-toast>');
37
+ const item = await openAndGetItem(el, 'Saved!', 'success');
38
+
39
+ expect(item.shadowRoot.querySelector('.text').textContent.trim()).toBe('Saved!');
40
+ expect(item.shadowRoot.querySelector('div').classList.contains('success')).toBe(true);
41
+ });
42
+
43
+ it('renders a close icon when showCloseIcon is set', async () => {
44
+ const el = await renderToast('<dile-toast duration="10000" showCloseIcon></dile-toast>');
45
+ const item = await openAndGetItem(el, 'Hello');
46
+
47
+ expect(item.shadowRoot.querySelector('dile-icon.closeicon')).toBeTruthy();
48
+ });
49
+
50
+ it('closing a toast dispatches dile-toast-closed-by-user with the message detail', async () => {
51
+ const el = await renderToast('<dile-toast duration="10000" showCloseIcon></dile-toast>');
52
+ const item = await openAndGetItem(el, 'Dismiss me', 'error');
53
+
54
+ let detail = null;
55
+ el.addEventListener('dile-toast-closed-by-user', (e) => { detail = e.detail; });
56
+
57
+ item.shadowRoot.querySelector('dile-icon.closeicon').click();
58
+ await el.updateComplete;
59
+
60
+ expect(detail).toBeTruthy();
61
+ expect(detail.message.text).toBe('Dismiss me');
62
+ expect(detail.message.toastType).toBe('error');
63
+ });
64
+
65
+ it('removes the closed toast from the DOM shortly after, without waiting for its duration', async () => {
66
+ const el = await renderToast('<dile-toast duration="10000" showCloseIcon></dile-toast>');
67
+ const item = await openAndGetItem(el, 'Dismiss me');
68
+
69
+ item.shadowRoot.querySelector('dile-icon.closeicon').click();
70
+ await el.updateComplete;
71
+ expect(item.shadowRoot.querySelector('div').classList.contains('hidden')).toBe(true);
72
+
73
+ await wait(600);
74
+
75
+ expect(el.shadowRoot.querySelector('dile-toast-item')).toBeNull();
76
+ });
77
+
78
+ it('still auto-hides and cleans up a toast on its own after duration elapses', async () => {
79
+ const el = await renderToast('<dile-toast duration="50"></dile-toast>');
80
+ await openAndGetItem(el, 'Auto dismiss');
81
+
82
+ await wait(1200);
83
+
84
+ expect(el.shadowRoot.querySelector('dile-toast-item')).toBeNull();
85
+ });
86
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dile/ui",
3
- "version": "2.27.0",
3
+ "version": "2.27.2",
4
4
  "description": "UI Core components from dile-components.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -27,5 +27,5 @@
27
27
  "publishConfig": {
28
28
  "access": "public"
29
29
  },
30
- "gitHead": "5c04395f81c01fd289869b1b99d427d482c516a5"
30
+ "gitHead": "f096749bccd6d56731d9f8509182686c05175348"
31
31
  }
package/styles/colors.css CHANGED
@@ -34,6 +34,8 @@
34
34
  --dile-on-terciary-dark-color: #ffffff;
35
35
  --dile-gray-dark-color: #555555;
36
36
  --dile-on-gray-dark-color: #ffffff;
37
+ --dile-gray-very-light-color: #f5f5f5;
38
+ --dile-on-gray-very-light-color: #303030;
37
39
  }
38
40
 
39
41
  :root.dark-theme {
@@ -78,4 +80,6 @@
78
80
 
79
81
  --dile-gray-dark-color: #cccccc;
80
82
  --dile-on-gray-dark-color: #1e1e1e;
83
+ --dile-gray-very-light-color: #404040;
84
+ --dile-on-gray-very-light-color: #ffffff;
81
85
  }