@kubex/zinc 1.1.129 → 1.1.131

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": "@kubex/zinc",
3
- "version": "1.1.129",
3
+ "version": "1.1.131",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -641,36 +641,32 @@ export default class ZnDataTable extends ZincElement {
641
641
  }
642
642
  }
643
643
 
644
- emptyState() {
645
- if (this.noInitialLoad && !this._initialLoad) {
646
- if (this.hasSlotController.test('no-results')) {
647
- return html`
648
- <div class="table--empty">
649
- <slot name="no-results"></slot>
650
- </div>`;
651
- }
652
-
653
- return html`
654
- <div class="table--empty">
655
- <zn-empty-state caption="No Results Found" icon="search">
656
- <p>Nothing matched your search. Try adjusting your filters.</p>
657
- </zn-empty-state>
658
- </div>`;
659
- }
644
+ // A table narrowed by a search or filter has rows, just none matching them, so
645
+ // its empty state must not claim there is nothing to create
646
+ private get isNarrowed(): boolean {
647
+ return this.search.length > 0 || this.filter.length > 0;
648
+ }
660
649
 
661
- if (this.hasSlotController.test('empty-state')) {
650
+ emptyState() {
651
+ if (this.isNarrowed || (this.noInitialLoad && !this._initialLoad)) {
662
652
  return html`
663
653
  <div class="table--empty">
664
- <slot name="empty-state"></slot>
654
+ <slot name="no-results">
655
+ <zn-empty-state caption="No Results Found" icon="search">
656
+ <p>Nothing matched your search. Try adjusting your filters.</p>
657
+ </zn-empty-state>
658
+ </slot>
665
659
  </div>`;
666
660
  }
667
661
 
668
662
  return html`
669
663
  <div class="table--empty">
670
- <zn-empty-state
671
- caption="${this.emptyStateCaption ? this.emptyStateCaption : (this.caption ? "No " + this.caption.toLowerCase() + " found" : "No data found")}"
672
- icon="${this.emptyStateIcon}">
673
- </zn-empty-state>
664
+ <slot name="empty-state">
665
+ <zn-empty-state
666
+ caption="${this.emptyStateCaption ? this.emptyStateCaption : (this.caption ? "No " + this.caption.toLowerCase() + " found" : "No data found")}"
667
+ icon="${this.emptyStateIcon}">
668
+ </zn-empty-state>
669
+ </slot>
674
670
  </div>`;
675
671
  }
676
672
 
@@ -1323,9 +1319,9 @@ export default class ZnDataTable extends ZincElement {
1323
1319
  size="${ifDefined(size)}"
1324
1320
  color="${ifDefined(color)}"
1325
1321
  ${ref(el => {
1326
- if (!el) return;
1327
- tokens.forEach(t => el.setAttribute(t, ''));
1328
- })}
1322
+ if (!el) return;
1323
+ tokens.forEach(t => el.setAttribute(t, ''));
1324
+ })}
1329
1325
  title="${ifDefined(data.title)}"
1330
1326
  ></zn-icon> ${content}`;
1331
1327
  }
@@ -1345,7 +1341,7 @@ export default class ZnDataTable extends ZincElement {
1345
1341
  <zn-hover-container placement="${placement}" flip>
1346
1342
  ${content}
1347
1343
  <div slot="content">
1348
- ${unsafeHTML(data.hoverContent)}
1344
+ ${unsafeHTML(data.hoverContent)}
1349
1345
  </div>
1350
1346
  </zn-hover-container>`;
1351
1347
  }
@@ -230,6 +230,52 @@ describe('<zn-data-table>', () => {
230
230
  }
231
231
  });
232
232
 
233
+ it('shows a no-results state when a filter empties a normally loading table', async () => {
234
+ const originalFetch = window.fetch;
235
+ window.fetch = () => Promise.resolve(new Response(JSON.stringify({rows: [], page: 1, perPage: 10, total: 0}),
236
+ {status: 200, headers: {'Content-Type': 'application/json'}}));
237
+
238
+ try {
239
+ const el = await fixture<ZnDataTable>(html`
240
+ <zn-data-table data-uri="/test-data" headers='{"name": {"key": "name", "label": "Name"}}'>
241
+ <div slot="empty-state" id="nothing-created">Nothing created yet</div>
242
+ <div slot="no-results" id="nothing-matched">Nothing matched</div>
243
+ </zn-data-table>`);
244
+
245
+ await waitUntil(() => el.shadowRoot?.querySelector('slot[name="empty-state"]'));
246
+ expect(el.shadowRoot!.querySelector('slot[name="empty-state"]')).to.exist;
247
+
248
+ el.filter = 'eyJrZXkiOiJ0eXBlIn0=';
249
+ el.refresh();
250
+ await waitUntil(() => el.shadowRoot?.querySelector('slot[name="no-results"]'));
251
+
252
+ expect(el.shadowRoot!.querySelector('slot[name="no-results"]')).to.exist;
253
+ expect(el.shadowRoot!.querySelector('slot[name="empty-state"]')).to.not.exist;
254
+ } finally {
255
+ window.fetch = originalFetch;
256
+ }
257
+ });
258
+
259
+ it('shows a no-results state when a search empties a normally loading table', async () => {
260
+ const originalFetch = window.fetch;
261
+ window.fetch = () => Promise.resolve(new Response(JSON.stringify({rows: [], page: 1, perPage: 10, total: 0}),
262
+ {status: 200, headers: {'Content-Type': 'application/json'}}));
263
+
264
+ try {
265
+ const el = await fixture<ZnDataTable>(html`
266
+ <zn-data-table data-uri="/test-data" search="nothing" headers='{"name": {"key": "name", "label": "Name"}}'>
267
+ <div slot="empty-state">Nothing created yet</div>
268
+ </zn-data-table>`);
269
+
270
+ await waitUntil(() => el.shadowRoot?.querySelector('zn-empty-state'));
271
+
272
+ expect(el.shadowRoot!.querySelector('slot[name="empty-state"]')).to.not.exist;
273
+ expect(el.shadowRoot!.querySelector('zn-empty-state')!.getAttribute('caption')).to.equal('No Results Found');
274
+ } finally {
275
+ window.fetch = originalFetch;
276
+ }
277
+ });
278
+
233
279
  it('exposes displayTemplates as an object property', async () => {
234
280
  const el = await fixture<ZnDataTable>(html` <zn-data-table></zn-data-table> `);
235
281
  const templates = (el as unknown as {displayTemplates: Record<string, unknown>}).displayTemplates;
@@ -1,7 +1,7 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
3
  import {HasSlotController} from "../../internal/slot";
4
- import {property} from 'lit/decorators.js';
4
+ import {property, state} from 'lit/decorators.js';
5
5
  import ZincElement from '../../internal/zinc-element';
6
6
 
7
7
  import styles from './panel.scss';
@@ -43,8 +43,13 @@ export default class ZnPanel extends ZincElement {
43
43
  @property({type: Boolean}) transparent: boolean;
44
44
  @property({type: Boolean}) shadow: boolean;
45
45
 
46
+ @state() private bodyEmpty = false;
47
+
48
+ private resizeObserver: ResizeObserver | null = null;
49
+
46
50
  protected firstUpdated(_changedProperties: PropertyValues) {
47
51
  super.firstUpdated(_changedProperties);
52
+ this.observeBodyContent();
48
53
  if (this.basis) {
49
54
  this.style.setProperty('--zn-panel-basis', this.basis.toString() + 'px');
50
55
  }
@@ -63,6 +68,10 @@ export default class ZnPanel extends ZincElement {
63
68
 
64
69
  connectedCallback() {
65
70
  super.connectedCallback();
71
+ if (this.hasUpdated) {
72
+ this.observeBodyContent();
73
+ }
74
+
66
75
  if (window.CSS.registerProperty) {
67
76
  try {
68
77
  window.CSS.registerProperty({
@@ -77,14 +86,42 @@ export default class ZnPanel extends ZincElement {
77
86
  }
78
87
  }
79
88
 
89
+ disconnectedCallback() {
90
+ super.disconnectedCallback();
91
+ this.resizeObserver?.disconnect();
92
+ this.resizeObserver = null;
93
+ }
94
+
95
+ private get bodySlot(): HTMLSlotElement | null {
96
+ return this.renderRoot?.querySelector('.panel__body > slot') ?? null;
97
+ }
98
+
99
+ // Slotted content can grow without the body's own box changing, so the observers go on the
100
+ // slotted elements rather than the body.
101
+ private observeBodyContent() {
102
+ const slot = this.bodySlot;
103
+ if (!slot) return;
104
+
105
+ this.resizeObserver ??= new ResizeObserver(() => this.measureBodyContent());
106
+ this.resizeObserver.disconnect();
107
+ slot.assignedElements({flatten: true}).forEach(el => this.resizeObserver!.observe(el));
108
+ this.measureBodyContent();
109
+ }
110
+
111
+ private measureBodyContent() {
112
+ const nodes = this.bodySlot?.assignedNodes({flatten: true}) ?? [];
113
+ this.bodyEmpty = !nodes.some(node => node.nodeType === Node.TEXT_NODE
114
+ ? node.textContent!.trim() !== ''
115
+ : (node as Element).getBoundingClientRect().height > 0);
116
+ }
117
+
80
118
  protected render(): unknown {
81
119
  const hasActionSlot = this.hasSlotController.test('actions');
82
120
  const hasFooterSlot = this.hasSlotController.test('footer');
83
121
  const hasHeader = this.caption || hasActionSlot;
84
- // Transparent panels with no vertical body padding have nothing to separate the header from,
85
- // so the underline is just a stray rule across the page.
122
+ const isBodyEmpty = !this.hasSlotController.test('[default]') || this.bodyEmpty;
86
123
  const isFlushY = this.flush || this.tabbed || this.flushY;
87
- const underlineHeader = !this.headerBorderless && !(this.transparent && isFlushY);
124
+ const underlineHeader = !this.headerBorderless && !(this.transparent && isFlushY) && !isBodyEmpty;
88
125
 
89
126
  return html`
90
127
  <div class="${classMap({
@@ -97,6 +134,7 @@ export default class ZnPanel extends ZincElement {
97
134
  'panel--transparent': this.transparent,
98
135
  'panel--has-actions': hasActionSlot,
99
136
  'panel--has-footer': hasFooterSlot,
137
+ 'panel--empty': isBodyEmpty,
100
138
  'panel--has-header': hasHeader,
101
139
  'panel--borderless-header': this.headerBorderless,
102
140
  'panel--cosmic': this.cosmic,
@@ -120,7 +158,7 @@ export default class ZnPanel extends ZincElement {
120
158
 
121
159
  <div class="panel__content">
122
160
  <div class="panel__body">
123
- <slot></slot>
161
+ <slot @slotchange="${this.observeBodyContent}"></slot>
124
162
  </div>
125
163
  </div>
126
164
 
@@ -111,6 +111,12 @@
111
111
  padding-top: 0;
112
112
  }
113
113
 
114
+ &--empty {
115
+ .panel__body {
116
+ padding: 0;
117
+ }
118
+ }
119
+
114
120
  &--flush {
115
121
  .panel__body {
116
122
  padding: 0;
@@ -1,5 +1,5 @@
1
1
  import '../../../dist/zn.min.js';
2
- import { expect, fixture, html } from '@open-wc/testing';
2
+ import { expect, fixture, html, waitUntil } from '@open-wc/testing';
3
3
 
4
4
  describe('<zn-panel>', () => {
5
5
  it('should render a component', async () => {
@@ -9,16 +9,52 @@ describe('<zn-panel>', () => {
9
9
  });
10
10
 
11
11
  it('underlines panel headers by default', async () => {
12
- const el = await fixture(html` <zn-panel caption="Example"></zn-panel> `);
12
+ const el = await fixture(html` <zn-panel caption="Example"><p>Content</p></zn-panel> `);
13
13
  const header = el.shadowRoot!.querySelector('.panel__header')!;
14
14
 
15
15
  expect(header.classList.contains('panel__header--underline')).to.be.true;
16
16
  });
17
17
 
18
18
  it('removes the header underline when header-borderless is set', async () => {
19
- const el = await fixture(html` <zn-panel caption="Example" header-borderless></zn-panel> `);
19
+ const el = await fixture(html` <zn-panel caption="Example" header-borderless><p>Content</p></zn-panel> `);
20
+ const header = el.shadowRoot!.querySelector('.panel__header')!;
21
+
22
+ expect(header.classList.contains('panel__header--underline')).to.be.false;
23
+ });
24
+
25
+ it('removes the header underline and body padding when there is no content', async () => {
26
+ const el = await fixture(html` <zn-panel caption="Example"></zn-panel> `);
20
27
  const header = el.shadowRoot!.querySelector('.panel__header')!;
21
28
 
22
29
  expect(header.classList.contains('panel__header--underline')).to.be.false;
30
+ expect(el.shadowRoot!.querySelector('.panel')!.classList.contains('panel--empty')).to.be.true;
31
+ });
32
+
33
+ it('removes the header underline when slotted content renders nothing', async () => {
34
+ const el = await fixture(html` <zn-panel caption="Example">
35
+ <div id="wrapper"></div>
36
+ </zn-panel> `);
37
+ const header = el.shadowRoot!.querySelector('.panel__header')!;
38
+
39
+ await waitUntil(() => !header.classList.contains('panel__header--underline'));
40
+
41
+ expect(el.shadowRoot!.querySelector('.panel')!.classList.contains('panel--empty')).to.be.true;
42
+ });
43
+
44
+ it('restores the header underline when slotted content gains height', async () => {
45
+ const el = await fixture(html` <zn-panel caption="Example">
46
+ <div id="wrapper"></div>
47
+ </zn-panel> `);
48
+ const header = el.shadowRoot!.querySelector('.panel__header')!;
49
+
50
+ await waitUntil(() => !header.classList.contains('panel__header--underline'));
51
+
52
+ const child = document.createElement('div');
53
+ child.style.height = '20px';
54
+ el.querySelector('#wrapper')!.append(child);
55
+
56
+ await waitUntil(() => header.classList.contains('panel__header--underline'));
57
+
58
+ expect(el.shadowRoot!.querySelector('.panel')!.classList.contains('panel--empty')).to.be.false;
23
59
  });
24
60
  });