@tipolox/litgrid-web 1.1.0 → 1.2.1

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.
@@ -7,6 +7,11 @@ import {
7
7
  type GridConfig,
8
8
  type GridFilter,
9
9
  type GridSelection,
10
+ type GridRowKey,
11
+ type SelectionChangeDetail,
12
+ type GridExternalState,
13
+ type GridQueryChangeDetail,
14
+ type SortDirection,
10
15
  type SelectionIntent
11
16
  } from '@tipolox/litgrid-core'
12
17
  import {
@@ -118,6 +123,7 @@ export class DataGrid extends LitElement {
118
123
  private previousBodyCursor = ''
119
124
  private previousBodyUserSelect = ''
120
125
  private activeHeaderMenu: string | null = null
126
+ private headerMenuCapabilities = { sortable: true, filterable: true }
121
127
  private columnChooserOpen = false
122
128
  private columnChooserPosition: { left: number; top: number } | null = null
123
129
  private quickSearchVisible = false
@@ -145,6 +151,7 @@ export class DataGrid extends LitElement {
145
151
  private shouldRestoreHeaderMenuFocus = false
146
152
  private screenReaderMessage = ''
147
153
  private lastScreenReaderAnnouncement = ''
154
+ private paginationFocusTarget: HTMLButtonElement | null = null
148
155
 
149
156
  // Use a larger cap (2^26) to avoid hitting browser element-height limits.
150
157
 
@@ -171,6 +178,21 @@ export class DataGrid extends LitElement {
171
178
  this.ariaLabel = 'Data grid'
172
179
  this.ariaDescription = ''
173
180
  this.screenReaderAnnouncements = true
181
+ this.engine.onSelectionChange(detail => {
182
+ this.requestUpdate()
183
+ this.dispatchEvent(new CustomEvent<SelectionChangeDetail>('selection-change', {
184
+ detail, bubbles: true, composed: true
185
+ }))
186
+ })
187
+ this.engine.onQueryChange(detail => {
188
+ this.quickSearchDraft = detail.query.search
189
+ this.clearBestFitWidths()
190
+ this.resetVerticalScroll()
191
+ this.requestUpdate()
192
+ this.dispatchEvent(new CustomEvent<GridQueryChangeDetail>('query-change', {
193
+ detail, bubbles: true, composed: true
194
+ }))
195
+ })
174
196
  }
175
197
 
176
198
  static styles = [lightThemeStyles, darkThemeStyles, css`
@@ -191,11 +213,68 @@ export class DataGrid extends LitElement {
191
213
  overflow: hidden;
192
214
  }
193
215
 
216
+ .viewport-frame {
217
+ position: relative;
218
+ min-width: 0;
219
+ }
220
+
194
221
  .viewport {
195
222
  overflow: auto;
196
223
  height: var(--grid-height, 320px);
197
224
  }
198
225
 
226
+ /* The transient external status covers only the body and never enters the
227
+ normal layout flow. Header/search/footer positions remain unchanged. */
228
+ .external-status {
229
+ position: absolute;
230
+ inset: 0;
231
+ z-index: 2;
232
+ box-sizing: border-box;
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ padding: 16px;
237
+ background: var(--litgrid-color-surface);
238
+ color: var(--litgrid-color-text-strong);
239
+ text-align: center;
240
+ pointer-events: none;
241
+ }
242
+
243
+ .external-status-label {
244
+ display: inline-flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ gap: 10px;
248
+ max-width: 100%;
249
+ border: 1px solid var(--litgrid-color-border);
250
+ border-radius: 8px;
251
+ padding: 10px 14px;
252
+ background: var(--litgrid-color-surface-subtle);
253
+ font-weight: 600;
254
+ }
255
+
256
+ .external-status.loading .external-status-label::before {
257
+ content: '';
258
+ box-sizing: border-box;
259
+ flex: none;
260
+ width: 16px;
261
+ height: 16px;
262
+ border: 2px solid var(--litgrid-color-border-strong);
263
+ border-top-color: var(--litgrid-color-accent);
264
+ border-radius: 50%;
265
+ animation: litgrid-external-loading-spin 0.8s linear infinite;
266
+ }
267
+
268
+ @keyframes litgrid-external-loading-spin {
269
+ to { transform: rotate(360deg); }
270
+ }
271
+
272
+ @media (prefers-reduced-motion: reduce) {
273
+ .external-status.loading .external-status-label::before {
274
+ animation: none;
275
+ }
276
+ }
277
+
199
278
  .screen-reader-only {
200
279
  position: absolute;
201
280
  width: 1px;
@@ -701,7 +780,17 @@ export class DataGrid extends LitElement {
701
780
  this.updateScrollbarWidth()
702
781
  }
703
782
 
783
+ willUpdate() {
784
+ // Reconcile after schema and persisted visibility settle, before triggers disappear.
785
+ this.reconcileHeaderMenuCapabilities()
786
+ }
787
+
704
788
  updated() {
789
+ const paginationTarget = this.paginationFocusTarget
790
+ this.paginationFocusTarget = null
791
+ if (paginationTarget?.isConnected && paginationTarget.disabled) {
792
+ this.renderRoot.querySelector<HTMLElement>('.pagination button:not([disabled]), .pagination select')?.focus()
793
+ }
705
794
  this.updateScrollbarWidth()
706
795
 
707
796
  if (this.shouldFocusViewport) {
@@ -1162,6 +1251,7 @@ export class DataGrid extends LitElement {
1162
1251
  })
1163
1252
  }
1164
1253
  const isOpening = this.activeHeaderMenu !== columnKey
1254
+ this.headerMenuCapabilities = this.getColumnCapabilities(columnKey)
1165
1255
 
1166
1256
  this.activeHeaderMenu = isOpening ? columnKey : null
1167
1257
  this.headerMenuTrigger = isOpening && event.currentTarget instanceof HTMLButtonElement
@@ -1171,6 +1261,20 @@ export class DataGrid extends LitElement {
1171
1261
  this.requestUpdate()
1172
1262
  }
1173
1263
 
1264
+ private getColumnCapabilities(columnKey: string) {
1265
+ const column = this.getResolvedColumns().find(column => column.key === columnKey)
1266
+ return { sortable: !!column && column.sortable !== false, filterable: !!column && column.filterable !== false }
1267
+ }
1268
+
1269
+ private reconcileHeaderMenuCapabilities() {
1270
+ if (this.activeHeaderMenu === null) return
1271
+ const next = this.getColumnCapabilities(this.activeHeaderMenu)
1272
+ if (!this.isColumnVisible(this.activeHeaderMenu)
1273
+ || next.sortable !== this.headerMenuCapabilities.sortable || next.filterable !== this.headerMenuCapabilities.filterable) {
1274
+ this.closeHeaderMenu(true)
1275
+ }
1276
+ }
1277
+
1174
1278
  private closeHeaderMenu(restoreFocus = false) {
1175
1279
  if (this.activeHeaderMenu === null && !this.columnChooserOpen) return
1176
1280
  this.activeHeaderMenu = null
@@ -1320,6 +1424,10 @@ export class DataGrid extends LitElement {
1320
1424
 
1321
1425
  private announceRowSelection(rowIndex: number, intent: SelectionIntent = 'replace') {
1322
1426
  const anchor = this.engine.getSelection().anchorRowIndex ?? rowIndex
1427
+ if (intent === 'range' && this.engine.getConfig().selection.rowKey !== undefined) {
1428
+ this.announceScreenReader(`${this.getDisplayedSelectedRowCount()} displayed rows selected.`)
1429
+ return
1430
+ }
1323
1431
  this.announceScreenReader(intent === 'range' && anchor !== rowIndex
1324
1432
  ? `Rows ${Math.min(anchor, rowIndex) + 1} through ${Math.max(anchor, rowIndex) + 1} selected.`
1325
1433
  : `Row ${rowIndex + 1} ${this.engine.isRowSelected(rowIndex) ? 'selected' : 'deselected'}.`)
@@ -1330,6 +1438,10 @@ export class DataGrid extends LitElement {
1330
1438
  }
1331
1439
 
1332
1440
  private announceVisibleRowCount(context: string) {
1441
+ if (this.engine.getConfig().queryMode === 'external') {
1442
+ this.announceScreenReader(`${context}. Server query requested.`)
1443
+ return
1444
+ }
1333
1445
  const rowCount = this.engine.getRowCount()
1334
1446
  this.announceScreenReader(rowCount === 0 ? `${context}. No matching rows.` : `${context}. ${rowCount} ${rowCount === 1 ? 'row' : 'rows'} shown.`)
1335
1447
  }
@@ -1337,22 +1449,25 @@ export class DataGrid extends LitElement {
1337
1449
 
1338
1450
 
1339
1451
  private handleSortAscending(columnKey: string) {
1452
+ if (!this.getColumnCapabilities(columnKey).sortable) return
1340
1453
  this.engine.sortBy(columnKey, 'asc')
1341
- this.announceScreenReader(`${this.getColumnLabel(columnKey)} sorted ascending.`)
1454
+ this.announceScreenReader(`${this.getColumnLabel(columnKey)} ${this.engine.getConfig().queryMode === 'external' ? 'ascending sort requested' : 'sorted ascending'}.`)
1342
1455
  this.requestUpdate()
1343
1456
  this.closeHeaderMenu(true)
1344
1457
  }
1345
1458
 
1346
1459
  private handleSortDescending(columnKey: string) {
1460
+ if (!this.getColumnCapabilities(columnKey).sortable) return
1347
1461
  this.engine.sortBy(columnKey, 'desc')
1348
- this.announceScreenReader(`${this.getColumnLabel(columnKey)} sorted descending.`)
1462
+ this.announceScreenReader(`${this.getColumnLabel(columnKey)} ${this.engine.getConfig().queryMode === 'external' ? 'descending sort requested' : 'sorted descending'}.`)
1349
1463
  this.requestUpdate()
1350
1464
  this.closeHeaderMenu(true)
1351
1465
  }
1352
1466
 
1353
- private handleClearSort() {
1467
+ private handleClearSort(columnKey: string) {
1468
+ if (!this.getColumnCapabilities(columnKey).sortable) return
1354
1469
  this.engine.clearSort()
1355
- this.announceScreenReader('Sorting cleared.')
1470
+ this.announceScreenReader(this.engine.getConfig().queryMode === 'external' ? 'Clear sorting requested.' : 'Sorting cleared.')
1356
1471
  this.requestUpdate()
1357
1472
  this.closeHeaderMenu(true)
1358
1473
  }
@@ -1560,7 +1675,8 @@ export class DataGrid extends LitElement {
1560
1675
  const threshold = Math.max(0, Math.floor(this.quickSearchDebounceThreshold))
1561
1676
  const debounceMs = Math.max(0, Math.floor(this.quickSearchDebounceMs))
1562
1677
 
1563
- if (this.sourceRowCount < threshold || debounceMs === 0) {
1678
+ const external = this.engine.getConfig().queryMode === 'external'
1679
+ if (debounceMs === 0 || (!external && this.sourceRowCount < threshold)) {
1564
1680
  this.applyQuickSearch(query)
1565
1681
  return
1566
1682
  }
@@ -1596,19 +1712,57 @@ export class DataGrid extends LitElement {
1596
1712
  return this.engine.getFilters()
1597
1713
  }
1598
1714
 
1715
+ sortBy(columnKey: string, direction: SortDirection) {
1716
+ this.engine.sortBy(columnKey, direction)
1717
+ this.resetVerticalScroll()
1718
+ this.requestUpdate()
1719
+ }
1720
+
1721
+ clearSort() {
1722
+ this.engine.clearSort()
1723
+ this.resetVerticalScroll()
1724
+ this.requestUpdate()
1725
+ }
1726
+
1727
+ getQuery() { return this.engine.getQuery() }
1728
+ refreshQuery() { this.engine.refreshQuery() }
1729
+ get externalState() { return this.engine.getExternalState() }
1730
+ set externalState(value: GridExternalState | null) {
1731
+ if (value) this.setExternalState(value)
1732
+ }
1733
+ setExternalState(value: GridExternalState): boolean {
1734
+ if (!this.engine.setExternalState(value)) return false
1735
+ this.clearQuickSearchDebounce()
1736
+ this.quickSearchDraft = this.engine.getQuickSearch()
1737
+ this.filterDrafts = new Map(this.engine.getFilters().map(filter => [filter.columnKey, filter]))
1738
+ this.sourceRowCount = this.engine.getRowCount()
1739
+ this.clearBestFitWidths()
1740
+ this.requestUpdate()
1741
+ return true
1742
+ }
1743
+
1744
+ private getPageLabel() {
1745
+ const pagination = this.engine.getPagination()
1746
+ if (this.engine.getConfig().queryMode === 'external' && pagination.totalKnown && pagination.pageIndex > 0 && pagination.pageIndex >= pagination.pageCount) return `Page ${pagination.pageIndex + 1} (out of range)`
1747
+ return pagination.totalKnown === false ? `Page ${pagination.pageIndex + 1}`
1748
+ : `Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}`
1749
+ }
1750
+
1599
1751
  setPage(pageIndex: number) {
1752
+ this.rememberPaginationFocus()
1600
1753
  this.engine.setPage(pageIndex)
1601
1754
  this.resetVerticalScroll()
1602
1755
  const pagination = this.engine.getPagination()
1603
- this.announceScreenReader(`Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}.`)
1756
+ this.announceScreenReader(`${this.getPageLabel()}.`)
1604
1757
  this.requestUpdate()
1605
1758
  }
1606
1759
 
1607
1760
  setPageSize(pageSize: number) {
1761
+ this.rememberPaginationFocus()
1608
1762
  this.engine.setPageSize(pageSize)
1609
1763
  this.resetVerticalScroll()
1610
1764
  const pagination = this.engine.getPagination()
1611
- this.announceScreenReader(`${pagination.pageSize} rows per page. Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}.`)
1765
+ this.announceScreenReader(`${pagination.pageSize} rows per page. ${this.getPageLabel()}.`)
1612
1766
  this.requestUpdate()
1613
1767
  }
1614
1768
 
@@ -1616,6 +1770,11 @@ export class DataGrid extends LitElement {
1616
1770
  return this.engine.getPagination()
1617
1771
  }
1618
1772
 
1773
+ private rememberPaginationFocus() {
1774
+ const active = this.shadowRoot?.activeElement
1775
+ if (active instanceof HTMLButtonElement && active.closest('.pagination')) this.paginationFocusTarget = active
1776
+ }
1777
+
1619
1778
  getTotalRowCount() {
1620
1779
  return this.engine.getTotalRowCount()
1621
1780
  }
@@ -1634,11 +1793,17 @@ export class DataGrid extends LitElement {
1634
1793
  this.requestUpdate()
1635
1794
  }
1636
1795
 
1796
+ private announceAllRowsSelected() {
1797
+ const count = this.engine.getSelectableRowCount()
1798
+ const qualifier = count === this.engine.getRowCount() ? '' : 'selectable '
1799
+ this.announceScreenReader(`All ${count} ${qualifier}displayed rows selected.`)
1800
+ }
1801
+
1637
1802
  selectAllRows() {
1638
- if (this.engine.getConfig().selection.mode !== 'multi-row' || this.engine.getRowCount() === 0) return
1639
- const unchanged = this.engine.getSelection().rowIndexes.size === this.engine.getRowCount()
1803
+ if (this.engine.getConfig().selection.mode !== 'multi-row' || this.engine.getSelectableRowCount() === 0) return
1804
+ const unchanged = this.engine.getSelection().rowIndexes.size === this.engine.getSelectableRowCount()
1640
1805
  this.engine.selectAllRows()
1641
- if (!unchanged) this.announceScreenReader(`All ${this.engine.getRowCount()} displayed rows selected.`)
1806
+ if (!unchanged) this.announceAllRowsSelected()
1642
1807
  this.requestUpdate()
1643
1808
  }
1644
1809
 
@@ -1654,6 +1819,22 @@ export class DataGrid extends LitElement {
1654
1819
  return this.engine.getSelection()
1655
1820
  }
1656
1821
 
1822
+ getSelectionSnapshot(): SelectionChangeDetail {
1823
+ return this.engine.getSelectionSnapshot()
1824
+ }
1825
+
1826
+ selectRowByKey(key: GridRowKey, intent?: SelectionIntent): boolean {
1827
+ const previous = this.engine.getSelectionSnapshot()
1828
+ const selected = this.engine.selectRowByKey(key, intent)
1829
+ const next = this.engine.getSelectionSnapshot()
1830
+ if (selected && next !== previous) {
1831
+ const index = next.rows.find(row => row.key === key)?.rowIndex
1832
+ ?? previous.rows.find(row => row.key === key)?.rowIndex
1833
+ if (index !== undefined) this.announceRowSelection(index, intent)
1834
+ }
1835
+ return selected
1836
+ }
1837
+
1657
1838
  isRowSelected(rowIndex: number) {
1658
1839
  return this.engine.isRowSelected(rowIndex)
1659
1840
  }
@@ -1713,6 +1894,7 @@ export class DataGrid extends LitElement {
1713
1894
  }
1714
1895
 
1715
1896
  private applyHeaderFilter(columnKey: string) {
1897
+ if (!this.getColumnCapabilities(columnKey).filterable) return
1716
1898
  const filter = this.getFilterDraft(columnKey)
1717
1899
  this.setFilter({
1718
1900
  ...filter,
@@ -1722,6 +1904,7 @@ export class DataGrid extends LitElement {
1722
1904
  }
1723
1905
 
1724
1906
  private clearHeaderFilter(columnKey: string) {
1907
+ if (!this.getColumnCapabilities(columnKey).filterable) return
1725
1908
  this.clearFilter(columnKey)
1726
1909
  this.closeHeaderMenu(true)
1727
1910
  }
@@ -2534,6 +2717,7 @@ export class DataGrid extends LitElement {
2534
2717
  }
2535
2718
 
2536
2719
  set data(value: unknown[]) {
2720
+ if (this.engine.getConfig().queryMode === 'external') return
2537
2721
  this.clearQuickSearchDebounce()
2538
2722
 
2539
2723
  this.sourceRowCount = Array.isArray(value) ? value.length : 0
@@ -2549,26 +2733,17 @@ export class DataGrid extends LitElement {
2549
2733
  }
2550
2734
 
2551
2735
  private getDisplayedSelectedRowCount() {
2552
- const rowCount = this.engine.getRowCount()
2553
- let selectedCount = 0
2554
-
2555
- for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
2556
- if (this.engine.isRowSelected(rowIndex)) {
2557
- selectedCount += 1
2558
- }
2559
- }
2560
-
2561
- return selectedCount
2736
+ return this.engine.getSelectionSnapshot().rowIndexes.length
2562
2737
  }
2563
2738
 
2564
2739
  private areAllDisplayedRowsSelected() {
2565
- const rowCount = this.engine.getRowCount()
2740
+ const rowCount = this.engine.getSelectableRowCount()
2566
2741
  return rowCount > 0 && this.getDisplayedSelectedRowCount() === rowCount
2567
2742
  }
2568
2743
 
2569
2744
  private hasPartiallySelectedDisplayedRows() {
2570
2745
  const selectedCount = this.getDisplayedSelectedRowCount()
2571
- return selectedCount > 0 && selectedCount < this.engine.getRowCount()
2746
+ return selectedCount > 0 && selectedCount < this.engine.getSelectableRowCount()
2572
2747
  }
2573
2748
 
2574
2749
  private handleSelectAllRows() {
@@ -2577,7 +2752,7 @@ export class DataGrid extends LitElement {
2577
2752
  this.announceScreenReader('All displayed rows deselected.')
2578
2753
  } else {
2579
2754
  this.engine.selectAllRows()
2580
- this.announceScreenReader(`All ${this.engine.getRowCount()} displayed rows selected.`)
2755
+ this.announceAllRowsSelected()
2581
2756
  }
2582
2757
  this.requestUpdate()
2583
2758
  }
@@ -2758,6 +2933,12 @@ export class DataGrid extends LitElement {
2758
2933
  render() {
2759
2934
  const count = this.engine.getRowCount()
2760
2935
  const pagination = this.engine.getPagination()
2936
+ const external = this.engine.getConfig().queryMode === 'external'
2937
+ const externalState = external ? this.engine.getExternalState() : null
2938
+ const outOfRange = external && pagination.totalKnown && pagination.pageIndex > 0 && pagination.pageIndex >= pagination.pageCount
2939
+ const externalStatus = externalState?.loading ? 'Loading results.' : externalState?.error
2940
+ ? externalState.error : outOfRange ? 'Requested page is out of range. Choose a previous page.'
2941
+ : count === 0 ? (pagination.totalRows === 0 ? 'No results.' : 'No rows on this page.') : ''
2761
2942
  const pageSizeOptions = [25, 50, 100].includes(pagination.pageSize)
2762
2943
  ? [25, 50, 100]
2763
2944
  : [pagination.pageSize, 25, 50, 100].sort((left, right) => left - right)
@@ -2881,6 +3062,7 @@ export class DataGrid extends LitElement {
2881
3062
  return html`
2882
3063
  <div
2883
3064
  class="shell"
3065
+ aria-busy=${external ? String(!!externalState?.loading) : nothing}
2884
3066
  style=${`--virtual-grid-template: ${gridTemplate}; --total-column-width: ${totalColumnWidth}px; --total-row-width: ${totalRowWidth}px; --row-header-width: ${rowHeaderWidth}px; --header-grid-template: ${headerGridTemplate}; --scrollbar-width: ${this.scrollbarWidth}px;`}
2885
3067
  @keydown=${this.handleShellKeyDown}
2886
3068
  >
@@ -2888,7 +3070,7 @@ export class DataGrid extends LitElement {
2888
3070
  ${this.screenReaderAnnouncements
2889
3071
  ? html`<div class="screen-reader-only" role="status" aria-live="polite" aria-atomic="true">${this.screenReaderMessage}</div>`
2890
3072
  : ''}
2891
- ${this.quickSearchVisible
3073
+ ${this.quickSearchVisible || external
2892
3074
  ? html`
2893
3075
  <div class="quick-search" role="search">
2894
3076
  <input
@@ -2938,7 +3120,7 @@ export class DataGrid extends LitElement {
2938
3120
  aria-label="Select all displayed rows"
2939
3121
  .checked=${this.areAllDisplayedRowsSelected()}
2940
3122
  .indeterminate=${this.hasPartiallySelectedDisplayedRows()}
2941
- ?disabled=${this.engine.getRowCount() === 0}
3123
+ ?disabled=${this.engine.getSelectableRowCount() === 0}
2942
3124
  @change=${() => this.handleSelectAllRows()}
2943
3125
  />`
2944
3126
  : html`<span>#</span>`}
@@ -3006,19 +3188,22 @@ export class DataGrid extends LitElement {
3006
3188
  ${this.quickSearchVisible ? 'Hide Quick Search' : 'Show Quick Search'}
3007
3189
  </button>
3008
3190
  <hr class="header-menu-divider" />
3009
- <button type="button" @click=${() => this.handleSortAscending(column.key)}>
3010
- Sort Asc
3011
- </button>
3012
- <button type="button" @click=${() => this.handleSortDescending(column.key)}>
3013
- Sort Desc
3014
- </button>
3015
- <button type="button" @click=${() => this.handleClearSort()}>
3016
- Clear Sort
3017
- </button>
3191
+ ${column.sortable === false ? '' : html`
3192
+ <button type="button" @click=${() => this.handleSortAscending(column.key)}>
3193
+ Sort Asc
3194
+ </button>
3195
+ <button type="button" @click=${() => this.handleSortDescending(column.key)}>
3196
+ Sort Desc
3197
+ </button>
3198
+ <button type="button" @click=${() => this.handleClearSort(column.key)}>
3199
+ Clear Sort
3200
+ </button>
3201
+ `}
3018
3202
  <button type="button" @click=${(event: MouseEvent) => this.toggleColumnChooser(event)}>
3019
3203
  Columns
3020
3204
  </button>
3021
3205
  ${(() => {
3206
+ if (column.filterable === false) return ''
3022
3207
  const draft = this.getFilterDraft(column.key)
3023
3208
  const valueFree = this.isValueFreeFilter(draft.operator)
3024
3209
 
@@ -3089,6 +3274,7 @@ export class DataGrid extends LitElement {
3089
3274
  <button class="columns-button" type="button" @click=${(event: MouseEvent) => this.toggleColumnChooser(event)}>Columns</button>
3090
3275
  </div>`
3091
3276
  : ''}
3277
+ <div class="viewport-frame">
3092
3278
  <div
3093
3279
  class="viewport"
3094
3280
  role="grid"
@@ -3142,11 +3328,15 @@ export class DataGrid extends LitElement {
3142
3328
  </div>
3143
3329
  </div>
3144
3330
  </div>
3331
+ ${external && externalStatus
3332
+ ? html`<div class=${externalState?.loading ? 'external-status loading' : 'external-status'} role=${externalState?.error ? 'alert' : 'status'} aria-atomic="true"><span class="external-status-label">${externalStatus}</span></div>`
3333
+ : nothing}
3334
+ </div>
3145
3335
  ${pagination.enabled
3146
3336
  ? html`
3147
3337
  <div class="pagination" role="navigation" aria-label="Pagination">
3148
3338
  <div class="pagination-summary">
3149
- <span>${pagination.totalRows} rows</span>
3339
+ <span>${pagination.totalKnown === false ? 'Total unknown' : `${pagination.totalRows} rows`}</span>
3150
3340
  <label>
3151
3341
  Rows per page
3152
3342
  <select
@@ -3163,7 +3353,7 @@ export class DataGrid extends LitElement {
3163
3353
  </label>
3164
3354
  </div>
3165
3355
  <div class="pagination-navigation">
3166
- <span>Page ${pagination.pageCount === 0 ? 0 : pagination.pageIndex + 1} of ${pagination.pageCount}</span>
3356
+ <span>${this.getPageLabel()}</span>
3167
3357
  <button
3168
3358
  type="button"
3169
3359
  ?disabled=${pagination.pageIndex === 0}
@@ -3171,7 +3361,7 @@ export class DataGrid extends LitElement {
3171
3361
  >Previous</button>
3172
3362
  <button
3173
3363
  type="button"
3174
- ?disabled=${pagination.pageCount === 0 || pagination.pageIndex >= pagination.pageCount - 1}
3364
+ ?disabled=${pagination.totalKnown === false ? !pagination.hasMore : pagination.pageCount === 0 || pagination.pageIndex >= pagination.pageCount - 1}
3175
3365
  @click=${() => this.setPage(pagination.pageIndex + 1)}
3176
3366
  >Next</button>
3177
3367
  </div>
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './components/DataGrid.js'
2
+ export type { GridQuery, GridQueryFilter, GridQueryReason, GridQueryChangeDetail, GridQuerySnapshot, GridExternalTotal, GridExternalState, SortDirection } from './types.js'
2
3
  export type {
3
4
  BestFitColumnWidth,
4
5
  ColumnReorderDetail,
@@ -8,6 +9,9 @@ export type {
8
9
  GridFilter,
9
10
  GridPaginationState,
10
11
  GridSelection,
12
+ GridRowKey,
13
+ SelectedRowSnapshot,
14
+ SelectionChangeDetail,
11
15
  SelectionIntent,
12
16
  GridColumn,
13
17
  GridTheme,
package/src/types.ts CHANGED
@@ -3,13 +3,23 @@ import type {
3
3
  GridFilter,
4
4
  GridPaginationState,
5
5
  GridSelection,
6
+ GridRowKey,
7
+ SelectionChangeDetail,
8
+ GridExternalState,
9
+ GridQuerySnapshot,
10
+ GridQueryChangeDetail,
11
+ SortDirection,
6
12
  SelectionIntent
7
13
  } from '@tipolox/litgrid-core'
8
14
  import type { ColumnStateChangeDetail, GridColumnState } from './columnState/columnState.js'
9
15
 
10
16
  export type { ColumnStateChangeDetail, GridColumnState }
17
+ export type { GridQuery, GridQueryFilter, GridQueryReason, GridQueryChangeDetail, GridQuerySnapshot, GridExternalTotal, GridExternalState, SortDirection } from '@tipolox/litgrid-core'
11
18
 
12
19
  export type {
20
+ GridRowKey,
21
+ SelectedRowSnapshot,
22
+ SelectionChangeDetail,
13
23
  GridConfig,
14
24
  GridFilter,
15
25
  GridPaginationState,
@@ -24,6 +34,10 @@ export type GridColumn = {
24
34
  header?: string
25
35
  width?: number | string
26
36
  hidden?: boolean
37
+ /** Show local sorting actions in the header menu. Defaults to true; does not change query state or imperative APIs. */
38
+ sortable?: boolean
39
+ /** Show local column filtering controls. Defaults to true; does not affect quick search or imperative APIs. */
40
+ filterable?: boolean
27
41
  accessor?: (row: unknown, rowIndex: number) => unknown
28
42
  render?: (value: unknown, row: unknown, rowIndex: number) => unknown
29
43
  }
@@ -47,6 +61,13 @@ export type ColumnVisibilityChangeDetail = {
47
61
  }
48
62
 
49
63
  export type DataGridElement = HTMLElement & {
64
+ /** Additive external-query capabilities; production DataGrid implements them. */
65
+ externalState?: GridExternalState | null
66
+ setExternalState?: (state: GridExternalState) => boolean
67
+ getQuery?: () => GridQuerySnapshot
68
+ refreshQuery?: () => void
69
+ sortBy?: (columnKey: string, direction: SortDirection) => void
70
+ clearSort?: () => void
50
71
  data: unknown[]
51
72
  columns: GridColumn[]
52
73
  config: GridConfig
@@ -103,6 +124,15 @@ export type DataGridElement = HTMLElement & {
103
124
  selectAllRows: () => void
104
125
  clearSelection: () => void
105
126
  getSelection: () => GridSelection
127
+ getSelectionSnapshot: () => SelectionChangeDetail
128
+ selectRowByKey: (key: GridRowKey, intent?: SelectionIntent) => boolean
106
129
  isRowSelected: (rowIndex: number) => boolean
107
130
  isCellSelected: (rowIndex: number, columnKey: string) => boolean
108
131
  }
132
+
133
+ declare global {
134
+ interface HTMLElementEventMap {
135
+ 'selection-change': CustomEvent<SelectionChangeDetail>
136
+ 'query-change': CustomEvent<GridQueryChangeDetail>
137
+ }
138
+ }