@kubex/zinc 2.0.0 → 2.0.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.
- package/dist/custom-elements.json +202 -197
- package/dist/vscode.html-custom-data.json +32 -32
- package/dist/web-types.json +67 -67
- package/dist/zn.d.ts +8 -6
- package/dist/zn.min.js +291 -290
- package/package.json +1 -1
- package/src/components/data-table/data-table.component.ts +48 -21
- package/src/components/data-table/data-table.scss +29 -8
- package/src/components/data-table/data-table.test.ts +54 -1
- package/src/internal/reorder.ts +10 -3
package/package.json
CHANGED
|
@@ -4,10 +4,11 @@ import {HasSlotController} from "../../internal/slot";
|
|
|
4
4
|
import {ifDefined} from "lit/directives/if-defined.js";
|
|
5
5
|
import {property, state} from 'lit/decorators.js';
|
|
6
6
|
import {ref} from "lit/directives/ref.js";
|
|
7
|
-
import {styleMap} from "lit/directives/style-map.js";
|
|
8
7
|
import {renderSortIcon, sortKeyHandler} from "../../internal/table-sort";
|
|
9
8
|
import {ReorderController} from "../../internal/reorder";
|
|
10
9
|
import {ResizeController} from '@lit-labs/observers/resize-controller.js';
|
|
10
|
+
import {slideTabIndicator, squishTabIndicator} from "../../internal/tab-indicator";
|
|
11
|
+
import {styleMap} from "lit/directives/style-map.js";
|
|
11
12
|
import {type TableTab, TabRowController} from "../../internal/table-tabs";
|
|
12
13
|
import {Task} from "@lit/task";
|
|
13
14
|
import {unsafeHTML} from "lit/directives/unsafe-html.js";
|
|
@@ -24,18 +25,18 @@ import ZnDataTableSearch from "../data-table-search";
|
|
|
24
25
|
import ZnDropdown from "../dropdown";
|
|
25
26
|
import ZnEmptyState from "../empty-state";
|
|
26
27
|
import ZnHoverContainer from "../hover-container";
|
|
28
|
+
import ZnIcon from "../icon";
|
|
29
|
+
import ZnInput from "../input";
|
|
27
30
|
import ZnMenu from "../menu";
|
|
28
31
|
import ZnMenuItem from "../menu-item";
|
|
32
|
+
import ZnOptGroup from "../opt-group";
|
|
33
|
+
import ZnOption from "../option";
|
|
29
34
|
import ZnPanel from "../panel";
|
|
35
|
+
import ZnSelect from "../select";
|
|
30
36
|
import ZnSkeleton from "../skeleton";
|
|
31
37
|
import ZnStyle from "../style";
|
|
32
38
|
import type ZnDataSelect from "../data-select";
|
|
33
|
-
import ZnIcon from "../icon";
|
|
34
|
-
import ZnInput from "../input";
|
|
35
|
-
import ZnOptGroup from "../opt-group";
|
|
36
|
-
import ZnOption from "../option";
|
|
37
39
|
import type ZnQueryBuilder from "../query-builder";
|
|
38
|
-
import ZnSelect from "../select";
|
|
39
40
|
|
|
40
41
|
// Tables load in such a way that the global sheet has to be re-applied inside the shadow root.
|
|
41
42
|
import bootStyles from '../../../scss/boot.scss';
|
|
@@ -920,6 +921,34 @@ export default class ZnDataTable extends ZincElement {
|
|
|
920
921
|
protected updated(changed: PropertyValues) {
|
|
921
922
|
super.updated(changed);
|
|
922
923
|
this.publishFilterSuggestions();
|
|
924
|
+
void this.syncPageIndicator();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Slides the shared active-page pill to the current page the way navbar pill tabs do
|
|
928
|
+
private async syncPageIndicator() {
|
|
929
|
+
const indicator = this.shadowRoot?.querySelector<HTMLElement>('.table__footer__indicator');
|
|
930
|
+
const active = indicator?.parentElement?.querySelector<ZnButton>('.table__footer__page--active');
|
|
931
|
+
if (!indicator || !active) return;
|
|
932
|
+
|
|
933
|
+
// The button has no size until it has rendered
|
|
934
|
+
await active.updateComplete;
|
|
935
|
+
if (!active.isConnected || !active.classList.contains('table__footer__page--active')) return;
|
|
936
|
+
|
|
937
|
+
const origin = indicator.parentElement!.getBoundingClientRect().left;
|
|
938
|
+
const tab = active.getBoundingClientRect();
|
|
939
|
+
const target = {left: tab.left - origin, right: tab.right - origin};
|
|
940
|
+
const left = `${target.left}px`; const width = `${target.right - target.left}px`;
|
|
941
|
+
if (indicator.style.left === left && indicator.style.width === width) return;
|
|
942
|
+
|
|
943
|
+
// A freshly rendered indicator has no width yet and jumps straight into place
|
|
944
|
+
const placed = indicator.style.width !== '';
|
|
945
|
+
const drawn = indicator.getBoundingClientRect();
|
|
946
|
+
indicator.style.left = left;
|
|
947
|
+
indicator.style.width = width;
|
|
948
|
+
if (placed) {
|
|
949
|
+
squishTabIndicator(indicator);
|
|
950
|
+
slideTabIndicator(indicator, {left: drawn.left - origin, right: drawn.right - origin}, target);
|
|
951
|
+
}
|
|
923
952
|
}
|
|
924
953
|
|
|
925
954
|
// The filter bar can't see the data, so hand it the values already on screen to type against
|
|
@@ -1459,28 +1488,26 @@ export default class ZnDataTable extends ZincElement {
|
|
|
1459
1488
|
</div>`;
|
|
1460
1489
|
}
|
|
1461
1490
|
|
|
1462
|
-
// A window of PAGE_WINDOW pages around the current one, plus the first and last pages;
|
|
1463
|
-
//
|
|
1491
|
+
// A window of PAGE_WINDOW pages around the current one, plus the first and last pages; null marks a gap.
|
|
1492
|
+
// Always PAGE_WINDOW + 4 slots once there are that many pages, so the prev/next buttons never move.
|
|
1464
1493
|
private getPageRange(): (number | null)[] {
|
|
1465
|
-
const
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
return
|
|
1473
|
-
|
|
1474
|
-
if (gap === 2) return [p - 1, p];
|
|
1475
|
-
return gap > 2 ? [null, p] : [p];
|
|
1476
|
-
});
|
|
1494
|
+
const n = this.totalPages;
|
|
1495
|
+
const slots = PAGE_WINDOW + 4;
|
|
1496
|
+
const range = (from: number, count: number) => Array.from({length: count}, (_, i) => from + i);
|
|
1497
|
+
if (n <= slots) return range(1, n);
|
|
1498
|
+
|
|
1499
|
+
const half = Math.floor(PAGE_WINDOW / 2);
|
|
1500
|
+
if (this.page - half <= 3) return [...range(1, slots - 2), null, n];
|
|
1501
|
+
if (this.page + half >= n - 2) return [1, null, ...range(n - slots + 3, slots - 2)];
|
|
1502
|
+
return [1, null, ...range(this.page - half, PAGE_WINDOW), null, n];
|
|
1477
1503
|
}
|
|
1478
1504
|
|
|
1479
1505
|
getPagination() {
|
|
1480
1506
|
if (this.hidePagination || this.totalPages <= 1) return null;
|
|
1481
1507
|
|
|
1482
1508
|
return html`
|
|
1483
|
-
<div class="table__footer__pages">
|
|
1509
|
+
<div class="table__footer__pages" style="--page-digits: ${String(this.totalPages).length}">
|
|
1510
|
+
<span class="table__footer__indicator"></span>
|
|
1484
1511
|
<zn-button class="table__footer__page"
|
|
1485
1512
|
color="transparent"
|
|
1486
1513
|
size="small"
|
|
@@ -311,7 +311,9 @@ table {
|
|
|
311
311
|
padding: 0;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// Equal shares either side keep the page buttons centred however wide the range label gets
|
|
314
315
|
&__left, &__rows-per-page {
|
|
316
|
+
flex: 1 1 0;
|
|
315
317
|
display: flex;
|
|
316
318
|
flex-direction: row;
|
|
317
319
|
align-items: center;
|
|
@@ -320,14 +322,33 @@ table {
|
|
|
320
322
|
}
|
|
321
323
|
|
|
322
324
|
&__pages {
|
|
325
|
+
// Every slot sized for the widest page number, so buttons don't resize as the window slides
|
|
326
|
+
--page-slot: max(var(--zn-button-height-small, 28px), calc(var(--page-digits, 1) * 1ch + 12px));
|
|
327
|
+
|
|
328
|
+
position: relative;
|
|
329
|
+
isolation: isolate;
|
|
323
330
|
display: flex;
|
|
324
331
|
align-items: center;
|
|
325
332
|
gap: 2px;
|
|
326
333
|
margin-inline: auto;
|
|
327
334
|
}
|
|
328
335
|
|
|
336
|
+
// Placed by left and width, slid with transform only
|
|
337
|
+
&__indicator {
|
|
338
|
+
@include wc.pill-tab-indicator-motion;
|
|
339
|
+
|
|
340
|
+
position: absolute;
|
|
341
|
+
inset-block: 0;
|
|
342
|
+
left: 0;
|
|
343
|
+
z-index: -1;
|
|
344
|
+
transform-origin: left center;
|
|
345
|
+
border-radius: var(--zn-border-radius);
|
|
346
|
+
background-color: var(--zn-table-page-active-background, var(--zn-color-neutral-300));
|
|
347
|
+
pointer-events: none;
|
|
348
|
+
}
|
|
349
|
+
|
|
329
350
|
&__rows-per-page {
|
|
330
|
-
|
|
351
|
+
justify-content: flex-end;
|
|
331
352
|
|
|
332
353
|
zn-select[name="rowPerPage"] {
|
|
333
354
|
--zn-input-background-color: rgb(var(--zn-panel));
|
|
@@ -346,13 +367,10 @@ table {
|
|
|
346
367
|
}
|
|
347
368
|
}
|
|
348
369
|
|
|
349
|
-
&__pages + &__rows-per-page {
|
|
350
|
-
margin-inline-start: 0;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
370
|
&__page::part(base) {
|
|
354
|
-
min-width: var(--
|
|
371
|
+
min-width: var(--page-slot);
|
|
355
372
|
padding: 0 5px;
|
|
373
|
+
font-variant-numeric: tabular-nums;
|
|
356
374
|
justify-content: center;
|
|
357
375
|
color: rgb(var(--zn-color-muted-text, var(--zn-text)));
|
|
358
376
|
font-size: 12px;
|
|
@@ -360,14 +378,14 @@ table {
|
|
|
360
378
|
}
|
|
361
379
|
|
|
362
380
|
&__page--active::part(base) {
|
|
363
|
-
background-color:
|
|
381
|
+
background-color: transparent;
|
|
364
382
|
color: var(--zn-table-page-active-color, rgb(var(--zn-text)));
|
|
365
383
|
}
|
|
366
384
|
|
|
367
385
|
&__gap {
|
|
368
386
|
display: inline-flex;
|
|
369
387
|
justify-content: center;
|
|
370
|
-
min-width: var(--
|
|
388
|
+
min-width: var(--page-slot);
|
|
371
389
|
font-weight: var(--zn-font-weight-semibold, 600);
|
|
372
390
|
}
|
|
373
391
|
}
|
|
@@ -529,6 +547,9 @@ zn-style.zn-muted {
|
|
|
529
547
|
|
|
530
548
|
.table tr .table__head--reorderable {
|
|
531
549
|
cursor: grab;
|
|
550
|
+
// A press here is a drag; starting a text selection would sweep it across the table
|
|
551
|
+
user-select: none;
|
|
552
|
+
-webkit-user-select: none;
|
|
532
553
|
|
|
533
554
|
&:active {
|
|
534
555
|
cursor: grabbing;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
+
import { centre, dragTo, drop, startDrag } from '../../internal/test-drag';
|
|
2
3
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
|
3
|
-
import { centre, drop, dragTo, startDrag } from '../../internal/test-drag';
|
|
4
4
|
import { render } from 'lit';
|
|
5
5
|
import type ZnDataTable from './data-table.component';
|
|
6
6
|
|
|
7
|
+
// WebKit reports a benign "ResizeObserver loop" error when paging re-lays out the footer's zn-select
|
|
8
|
+
window.addEventListener('error', (e: ErrorEvent) => {
|
|
9
|
+
if (typeof e.message === 'string' && e.message.includes('ResizeObserver loop')) {
|
|
10
|
+
e.stopImmediatePropagation();
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
}
|
|
13
|
+
}, true);
|
|
14
|
+
|
|
7
15
|
describe('<zn-data-table>', () => {
|
|
8
16
|
it('should render a component', async () => {
|
|
9
17
|
const el = await fixture(html` <zn-data-table></zn-data-table> `);
|
|
@@ -67,6 +75,51 @@ describe('<zn-data-table>', () => {
|
|
|
67
75
|
}
|
|
68
76
|
});
|
|
69
77
|
|
|
78
|
+
it('renders the same number of page slots on every page', async () => {
|
|
79
|
+
const originalFetch = window.fetch;
|
|
80
|
+
window.fetch = (_url, init) => {
|
|
81
|
+
const {page} = JSON.parse(String(init!.body)) as {page: number};
|
|
82
|
+
return Promise.resolve(new Response(JSON.stringify({
|
|
83
|
+
rows: [{id: '1', cells: [{text: 'Row', column: 'name'}]}],
|
|
84
|
+
page,
|
|
85
|
+
perPage: 10,
|
|
86
|
+
total: 1234,
|
|
87
|
+
}), {status: 200, headers: {'Content-Type': 'application/json'}}));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const el = await fixture<ZnDataTable>(html`
|
|
92
|
+
<zn-data-table data-uri="/test-data" method="POST" headers='{"name": {"key": "name", "label": "Name"}}'></zn-data-table>`);
|
|
93
|
+
const slots = () => [...el.shadowRoot!.querySelectorAll('.table__footer__page, .table__footer__gap')]
|
|
94
|
+
.slice(1, -1).map(s => s.textContent!.trim()).join(',');
|
|
95
|
+
|
|
96
|
+
const seen: Record<number, string> = {};
|
|
97
|
+
for (const page of [1, 4, 5, 60, 120, 121, 124]) {
|
|
98
|
+
(el as unknown as {goToPage: (p: number) => void}).goToPage(page);
|
|
99
|
+
await waitUntil(() => el.shadowRoot!.querySelector('[aria-current="page"]')?.textContent?.trim() === String(page));
|
|
100
|
+
seen[page] = slots();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
expect(seen).to.deep.equal({
|
|
104
|
+
1: '1,2,3,4,5,…,124',
|
|
105
|
+
4: '1,2,3,4,5,…,124',
|
|
106
|
+
5: '1,…,4,5,6,…,124',
|
|
107
|
+
60: '1,…,59,60,61,…,124',
|
|
108
|
+
120: '1,…,119,120,121,…,124',
|
|
109
|
+
121: '1,…,120,121,122,123,124',
|
|
110
|
+
124: '1,…,120,121,122,123,124',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const indicator = el.shadowRoot!.querySelector<HTMLElement>('.table__footer__indicator')!;
|
|
114
|
+
const active = el.shadowRoot!.querySelector<HTMLElement>('.table__footer__page--active')!;
|
|
115
|
+
await waitUntil(() => indicator.getAnimations().length === 0);
|
|
116
|
+
expect(Math.round(indicator.getBoundingClientRect().left)).to.equal(Math.round(active.getBoundingClientRect().left));
|
|
117
|
+
expect(Math.round(indicator.getBoundingClientRect().width)).to.equal(Math.round(active.getBoundingClientRect().width));
|
|
118
|
+
} finally {
|
|
119
|
+
window.fetch = originalFetch;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
70
123
|
it('hides the rows-per-page selector when the dataset fits the smallest page size', async () => {
|
|
71
124
|
const originalFetch = window.fetch;
|
|
72
125
|
window.fetch = () => Promise.resolve(new Response(JSON.stringify({
|
package/src/internal/reorder.ts
CHANGED
|
@@ -99,6 +99,7 @@ export class ReorderController implements ReactiveController {
|
|
|
99
99
|
private shield: HTMLElement | null = null;
|
|
100
100
|
private scroller: Element | null = null;
|
|
101
101
|
private scrollFrame = 0;
|
|
102
|
+
private pressRoot: Node | null = null;
|
|
102
103
|
|
|
103
104
|
constructor(host: ReorderHost, options: ReorderOptions = {}) {
|
|
104
105
|
this.host = host;
|
|
@@ -204,7 +205,11 @@ export class ReorderController implements ReactiveController {
|
|
|
204
205
|
window.addEventListener('pointercancel', this.onPointerCancel, true);
|
|
205
206
|
window.addEventListener('touchmove', this.onTouchMove, {capture: true, passive: false});
|
|
206
207
|
// A link or image in the row would otherwise start a native drag and take the pointer
|
|
207
|
-
window.addEventListener('dragstart', this.
|
|
208
|
+
window.addEventListener('dragstart', this.cancel, true);
|
|
209
|
+
// The press would otherwise also start a text selection that sweeps the page as it drags; selectstart isn't
|
|
210
|
+
// composed, so it's caught in the row's own tree
|
|
211
|
+
this.pressRoot = this.host.reorderRow(key)!.getRootNode();
|
|
212
|
+
this.pressRoot.addEventListener('selectstart', this.cancel, true);
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
private onPointerMove = (event: PointerEvent) => {
|
|
@@ -246,7 +251,7 @@ export class ReorderController implements ReactiveController {
|
|
|
246
251
|
if (this.isDragging) event.preventDefault();
|
|
247
252
|
};
|
|
248
253
|
|
|
249
|
-
private
|
|
254
|
+
private cancel = (event: Event) => {
|
|
250
255
|
event.preventDefault();
|
|
251
256
|
};
|
|
252
257
|
|
|
@@ -463,7 +468,9 @@ export class ReorderController implements ReactiveController {
|
|
|
463
468
|
window.removeEventListener('pointerup', this.onPointerUp, true);
|
|
464
469
|
window.removeEventListener('pointercancel', this.onPointerCancel, true);
|
|
465
470
|
window.removeEventListener('touchmove', this.onTouchMove, true);
|
|
466
|
-
window.removeEventListener('dragstart', this.
|
|
471
|
+
window.removeEventListener('dragstart', this.cancel, true);
|
|
472
|
+
this.pressRoot?.removeEventListener('selectstart', this.cancel, true);
|
|
473
|
+
this.pressRoot = null;
|
|
467
474
|
}
|
|
468
475
|
|
|
469
476
|
/** Shows the new order, sliding each row from its old position. */
|