@fusion.dev/helix-headless 0.1.0 → 0.3.0

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.
Files changed (127) hide show
  1. package/custom-elements.json +234 -234
  2. package/dist/accordion/item/accordion-item.js +0 -108
  3. package/dist/accordion/root/accordion-root.js +0 -33
  4. package/dist/announcement/root/announcement-root.js +1 -76
  5. package/dist/app-header/root/app-header-root.js +0 -84
  6. package/dist/avatar/group/avatar-group.js +0 -77
  7. package/dist/avatar/root/avatar-root.js +0 -72
  8. package/dist/badge/root/badge-root.js +0 -67
  9. package/dist/bar-chart/root/bar-chart-root.js +3 -263
  10. package/dist/blockquote/root/blockquote-root.js +0 -46
  11. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
  12. package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
  13. package/dist/button/root/button-root.js +0 -215
  14. package/dist/button-group/root/button-group-root.js +0 -30
  15. package/dist/callout/root/callout-root.js +0 -52
  16. package/dist/card/root/card-root.js +0 -41
  17. package/dist/carousel/root/carousel-root.d.ts +2 -2
  18. package/dist/carousel/root/carousel-root.js +2 -362
  19. package/dist/chat/composer/chat-composer.js +0 -131
  20. package/dist/chat/message/chat-message.js +0 -137
  21. package/dist/chat/root/chat-root.js +0 -193
  22. package/dist/checkbox/root/checkbox-root.js +0 -113
  23. package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
  24. package/dist/code/root/code-root.js +0 -26
  25. package/dist/code-block/root/code-block-root.js +0 -100
  26. package/dist/color-input/root/color-input-root.js +0 -217
  27. package/dist/color-picker/color.js +0 -69
  28. package/dist/color-picker/root/color-picker-root.js +0 -272
  29. package/dist/combobox/root/combobox-root.js +0 -428
  30. package/dist/command/root/command-root.js +0 -177
  31. package/dist/copy-button/root/copy-button-root.js +0 -134
  32. package/dist/data-table/root/data-table-root.js +0 -336
  33. package/dist/data-table/root/order.js +0 -100
  34. package/dist/date-input/root/date-input-root.js +1 -398
  35. package/dist/date-picker/root/date-picker-root.js +0 -358
  36. package/dist/dialog/root/dialog-root.js +0 -189
  37. package/dist/divider/root/divider-root.js +0 -71
  38. package/dist/donut-chart/root/donut-chart-root.js +0 -214
  39. package/dist/drawer/root/drawer-root.js +0 -116
  40. package/dist/drilldown/item/drilldown-item.js +0 -82
  41. package/dist/drilldown/root/drilldown-root.js +0 -91
  42. package/dist/element-events.generated.js +0 -5
  43. package/dist/empty/root/empty-root.js +0 -35
  44. package/dist/file-input/root/file-input-root.js +0 -232
  45. package/dist/format-date/root/format-date-root.js +0 -95
  46. package/dist/heatmap/root/buckets.js +0 -57
  47. package/dist/heatmap/root/heatmap-root.js +0 -313
  48. package/dist/icon/root/icon-root.js +0 -48
  49. package/dist/icon-button/root/icon-button-root.js +0 -30
  50. package/dist/image/root/image-root.js +0 -174
  51. package/dist/index.js +0 -9
  52. package/dist/input/root/input-root.js +0 -228
  53. package/dist/kbd/root/kbd-root.js +0 -117
  54. package/dist/legend/root/legend-root.js +0 -73
  55. package/dist/line-chart/root/line-chart-root.js +0 -172
  56. package/dist/list/item/list-item.js +0 -92
  57. package/dist/list/root/list-root.js +0 -26
  58. package/dist/listbox/root/listbox-root.js +2 -429
  59. package/dist/loader/root/loader-root.js +0 -40
  60. package/dist/megamenu/root/megamenu-root.js +0 -284
  61. package/dist/menu/item/menu-item.js +1 -221
  62. package/dist/menu/root/menu-root.js +1 -171
  63. package/dist/meter/root/meter-root.js +1 -152
  64. package/dist/navigation/root/navigation-root.js +0 -56
  65. package/dist/notifications/notification/notification.js +0 -159
  66. package/dist/notifications/root/notifications-root.js +0 -148
  67. package/dist/number-input/root/number-input-root.js +1 -263
  68. package/dist/option/group/option-group.js +0 -26
  69. package/dist/option/root/option-root.js +0 -157
  70. package/dist/outline/root/outline-root.js +0 -292
  71. package/dist/pagination/root/pagination-root.js +0 -361
  72. package/dist/pagination/size/pagination-size.js +0 -104
  73. package/dist/pagination/summary/pagination-summary.js +0 -90
  74. package/dist/pin-input/root/pin-input-root.js +0 -276
  75. package/dist/popover/root/popover-root.js +0 -103
  76. package/dist/progress/root/progress-root.js +1 -133
  77. package/dist/qr-code/root/qr-code-root.js +0 -96
  78. package/dist/quote/root/quote-root.js +0 -25
  79. package/dist/radio/group/radio-group.js +0 -81
  80. package/dist/radio/root/radio-root.js +0 -85
  81. package/dist/rating/root/rating-root.js +0 -220
  82. package/dist/scroll-area/root/scroll-area-root.js +0 -100
  83. package/dist/select/root/select-root.js +2 -218
  84. package/dist/skeleton/root/skeleton-root.js +0 -20
  85. package/dist/slider/root/slider-root.js +1 -267
  86. package/dist/sparkline/root/sparkline-root.js +3 -164
  87. package/dist/speed-dial/item/speed-dial-item.js +0 -52
  88. package/dist/speed-dial/root/speed-dial-root.js +1 -116
  89. package/dist/splitter/root/splitter-root.js +0 -489
  90. package/dist/stat/group/stat-group.js +0 -18
  91. package/dist/stat/root/stat-root.js +0 -23
  92. package/dist/steps/root/steps-root.js +0 -62
  93. package/dist/steps/step/step.js +0 -131
  94. package/dist/switch/root/switch-root.js +0 -80
  95. package/dist/table/cell/table-cell.js +0 -23
  96. package/dist/table/column/table-column.js +0 -158
  97. package/dist/table/root/table-root.js +0 -82
  98. package/dist/table/row/table-row.js +0 -21
  99. package/dist/tabs/panel/tab-panel.js +0 -49
  100. package/dist/tabs/root/tabs-root.js +0 -98
  101. package/dist/tabs/tab/tab.js +0 -59
  102. package/dist/tag/root/tag-root.js +0 -31
  103. package/dist/textarea/root/textarea-root.js +0 -162
  104. package/dist/time-input/root/time-input-root.js +0 -283
  105. package/dist/time-picker/root/time-picker-root.js +0 -326
  106. package/dist/timeline/item/timeline-item.js +0 -66
  107. package/dist/timeline/root/timeline-root.js +0 -41
  108. package/dist/toast/item/toast-item.js +0 -165
  109. package/dist/toast/root/toast-root.js +0 -51
  110. package/dist/toggle/group/toggle-group.js +0 -150
  111. package/dist/toggle/root/toggle-root.js +0 -78
  112. package/dist/toolbar/root/toolbar-root.js +0 -116
  113. package/dist/tooltip/root/tooltip-root.js +0 -427
  114. package/dist/tour/root/tour-root.js +0 -130
  115. package/dist/tour/stop/tour-stop.js +1 -127
  116. package/dist/tree/item/tree-item.js +0 -198
  117. package/dist/tree/root/tree-root.js +0 -97
  118. package/dist/trend/root/trend-root.js +0 -53
  119. package/dist/video/root/video-root.d.ts +8 -12
  120. package/dist/video/root/video-root.js +5 -756
  121. package/dist/video-playlist/item/video-playlist-item.js +1 -90
  122. package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
  123. package/dist/video-playlist/root/video-playlist-root.js +0 -113
  124. package/dist/virtual-list/root/virtual-list-root.js +0 -105
  125. package/jsx.d.ts +1408 -2576
  126. package/package.json +6 -6
  127. package/vue.d.ts +1 -1
@@ -9,176 +9,36 @@ import { customElement, HelixElement, HelixFilterEvent, HelixNavigateEvent, Heli
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { repeat } from 'lit/directives/repeat.js';
12
- /* `static-html`'s `html`, because the pagination's tag is interpolated: a
13
- template naming a literal `helix-pagination` renders the styled element from
14
- the headless tier, and `lit`'s own `html` treats `${tag}` in tag position as
15
- text, which renders an element called `lit$…$` that lays out as nothing.
16
- Measured: the styled spec read `localName` as `lit$640177303$`. */
17
12
  import { html } from 'lit/static-html.js';
18
13
  import '../../pagination/index.js';
19
14
  import { cycle, format, ordered, parse } from './order.js';
20
- /**
21
- * @summary A table that owns a result: rows as data, with sort, filter and paging.
22
- *
23
- * @description
24
- * **The schema is markup and the data is a property.** Columns are declared as
25
- * `helix-table-column` children, because a schema is small, declarative, and
26
- * something a reader should be able to see in the HTML; rows are handed over as
27
- * an array, because data is large and a table that draws ten thousand elements
28
- * cannot window them.
29
- *
30
- * ```html
31
- * <helix-data-table .rows=${rows} sort="name:asc" page-size="20">
32
- * <helix-table-column field="name" sortable>Name</helix-table-column>
33
- * <helix-table-column field="size" align="end" sortable .cell=${bytes}>Size</helix-table-column>
34
- * </helix-data-table>
35
- * ```
36
- *
37
- * `helix-table` takes rows as markup and is the right component for a table of
38
- * twelve things a person wrote out.
39
- *
40
- * **Sort and filter apply to the whole set, never the page.** Sorting the drawn
41
- * window is correct on the first screen and wrong on every other one, which is
42
- * how it survives being looked at. The arithmetic lives in `order.ts` where
43
- * nothing can see a window, and its spec asserts the composition rather than the
44
- * three steps, because each step alone passes either way.
45
- *
46
- * **What it does not do, stated where a consumer will read it: the cell values
47
- * do not server-render.** The headings do, because a column renders its own, and
48
- * the row elements do, because their count comes from `rows`. Reading a cell
49
- * needs the columns, and the columns are markup children that Lit SSR does not
50
- * hand this element: `children` is undefined there. So a crawler and a reader on
51
- * a slow connection see a table of the right shape with nothing in it, which is
52
- * strictly less than `helix-table` serves for a table whose rows are markup.
53
- *
54
- * That is the cost of "the schema is markup and the data is a property". Use
55
- * `helix-table` where the content has to be in the served HTML and the result is
56
- * small enough to write out; use this where the result is large, and accept that
57
- * the first paint is a shell. The obvious workaround, a `columns` property
58
- * beside the markup columns, is deliberately not offered: it splits one source
59
- * of truth for the columns into two that nothing checks against each other, and
60
- * a table rendering one way on the server and another in the browser is that
61
- * failure with a hydration mismatch on top.
62
- *
63
- * **Every axis is controlled or uncontrolled, and that is what makes a server
64
- * mode fall out rather than be bolted on.** `sort`, `filter`, `page` and
65
- * `page-size` each defer to the consumer when set and seed themselves from a
66
- * `default-*` when not. A consumer who answers all four holds the data flow:
67
- * they hear the reader's request, fetch, and hand back a new `rows` with a
68
- * `total`. Retrofitting that inverts the flow and is a rewrite, so it is here
69
- * from the start.
70
- *
71
- * @since 0.1.0
72
- * @status experimental
73
- * @category data
74
- * @aliases Grid, Result table, Data grid
75
- * @documentation https://helix.fusion.dev/components/data-table
76
- *
77
- * @slot - The columns, declared as `helix-table-column` children.
78
- * @slot footer - Below the grid, replacing the pagination this would otherwise draw.
79
- * @slot toolbar - Above the grid, for a search field or actions.
80
- *
81
- * @event {HelixFilterEvent} helix-filter - Fired when the query changes, and cancelable.
82
- * @event {HelixNavigateEvent} helix-navigate - Fired when a reader asks for another page, and cancelable.
83
- * @event {HelixSortEvent} helix-sort - Fired when a reader asks for a different order, and cancelable.
84
- *
85
- * @csspart cell - One cell.
86
- * @csspart empty - Shown when no rows match.
87
- * @csspart footer - The region below the grid.
88
- * @csspart head - The row the column headings sit in. `display: contents`, so it takes no space.
89
- * @csspart loading - Shown while the rows are still on their way.
90
- * @csspart pagination - The pagination, where this draws one.
91
- * @csspart row - One row of data.
92
- * @csspart table - The grid.
93
- * @csspart toolbar - The region above the grid.
94
- *
95
- * @cssproperty --data-table-template - The column track list this element worked out from the columns declared in it. Written by the component, read by the sheet that paints: a stylesheet takes it to lay the grid out, and setting it does nothing, because the component writes it inline on the very element that reads it and an inline declaration beats any sheet. `--data-table-columns` is the one to set.
96
- */
97
15
  let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollection(HelixElement) {
98
16
  constructor() {
99
17
  super(...arguments);
100
- /**
101
- * Query to open with, where nothing declared one.
102
- */
103
18
  this.defaultFilter = '';
104
- /**
105
- * Page to open on, where nothing declared one.
106
- */
107
19
  this.defaultPage = 1;
108
- /**
109
- * Page size to open with, where nothing declared one.
110
- */
111
20
  this.defaultPageSize = 0;
112
- /**
113
- * Order to open on, where nothing declared one.
114
- */
115
21
  this.defaultSort = '';
116
- /**
117
- * The field naming a row's stable identity.
118
- *
119
- * **Needed before selection exists, not after.** Lit keys the rendered rows by
120
- * it, so a sort moves a row's element rather than rewriting every cell in
121
- * place, which is what keeps focus inside a cell and keeps a CSS transition
122
- * attached to the row it belongs to. Left unset the index is the key, which is
123
- * correct only while nothing reorders: it is why this defaults to something
124
- * that works and is worth setting anyway.
125
- */
126
22
  this.key = '';
127
- /**
128
- * Accessible name for the grid.
129
- */
130
23
  this.label = '';
131
- /**
132
- * Whether this element does the work or asks the consumer to.
133
- *
134
- * `client` filters, sorts and pages `rows` itself. `server` applies none of
135
- * them: `rows` is the page, `total` is the size of the whole result, and the
136
- * three events are requests the consumer answers by fetching. Past some size
137
- * the client cannot hold the data, so this is not an optimization, it is the
138
- * only mode that works, and it is here from the start because adding it later
139
- * inverts the data flow.
140
- */
141
24
  this.mode = 'client';
142
- /**
143
- * The whole result, in the order it arrived.
144
- *
145
- * A property and not an attribute: it is an array. Left alone this element
146
- * filters, sorts and pages it; in `server` mode it is the page the consumer
147
- * already fetched and none of the three is applied.
148
- *
149
- * **Read, never written.** Sorting produces a new array rather than reordering
150
- * this one, because a consumer's array may be the same object their own state
151
- * holds and reordering it under them is a mutation they did not ask for.
152
- */
153
25
  this.rows = [];
154
- /**
155
- * How many rows the whole result has, where this element cannot count them.
156
- *
157
- * Only read in `server` mode, where `rows` is one page and its length says
158
- * nothing about the result. Ignored in `client` mode rather than trusted,
159
- * because two sources for one count is the mistake, and here the array is the
160
- * one that cannot be stale.
161
- */
162
26
  this.total = 0;
163
27
  this.sortState = undefined;
164
28
  this.filterState = undefined;
165
29
  this.pageState = undefined;
166
30
  this.pageSizeState = undefined;
167
31
  this.paginatedState = undefined;
168
- /** Said once per element, so a table that re-renders does not repeat it. */
169
32
  this.warnedAboutSize = false;
170
- /** Columns arrive from the light DOM, which changes no property here. */
171
33
  this.handleSlotChange = () => {
172
34
  this.requestUpdate();
173
35
  };
174
- /** A click on a sortable heading. */
175
36
  this.handleHeadClick = (event) => {
176
37
  const column = this.columnFrom(event);
177
38
  if (column?.canSort) {
178
39
  this.toggleSort(column.field);
179
40
  }
180
41
  };
181
- /** The same on Enter and Space, since a heading that sorts is a control. */
182
42
  this.handleHeadKey = (event) => {
183
43
  if (event.key !== 'Enter' && event.key !== ' ') {
184
44
  return;
@@ -189,14 +49,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
189
49
  this.toggleSort(column.field);
190
50
  }
191
51
  };
192
- /**
193
- * The composed pagination's request, relayed as this element's own.
194
- *
195
- * Stopped rather than allowed to bubble, because two `helix-navigate` events
196
- * for one click would reach a consumer's handler twice: the pagination's, and
197
- * this element's after it. The one that leaves carries this element's page
198
- * bounds rather than the inner component's.
199
- */
200
52
  this.handleNavigate = (event) => {
201
53
  event.stopPropagation();
202
54
  const wanted = Number(event.detail.value);
@@ -206,26 +58,10 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
206
58
  };
207
59
  }
208
60
  static { HelixHeadlessDataTable_1 = this; }
209
- /**
210
- * Nothing is assigned by name, so the columns can be put in a header row.
211
- *
212
- * `role="columnheader"` under `role="grid"` with no owning row is invalid, and
213
- * the columns are ordinary children a consumer writes with no `slot`. Manual
214
- * assignment is the only way to wrap them without editing the consumer's
215
- * markup, which is a change a React consumer's hydration reports and does not
216
- * patch up. The same arrangement `helix-table` arrived at, for the same reason.
217
- */
218
61
  static { this.shadowRootOptions = {
219
62
  ...HelixElement.shadowRootOptions,
220
63
  slotAssignment: 'manual',
221
64
  }; }
222
- /**
223
- * The order, as `field:direction`.
224
- *
225
- * Set it and this element defers: a reader's click fires `helix-sort` and
226
- * changes nothing until a new value arrives. `default-sort` seeds an order this
227
- * element then owns.
228
- */
229
65
  get sort() {
230
66
  return this.sortState ?? this.defaultSort;
231
67
  }
@@ -234,13 +70,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
234
70
  this.sortState = next;
235
71
  this.requestUpdate('sort', previous);
236
72
  }
237
- /**
238
- * What the rows are narrowed to, as one query across every column.
239
- *
240
- * One query rather than a predicate per column, which is what a reader means
241
- * by a search box above a table. It matches on each column's ordering value,
242
- * so search and sort cannot disagree about what a column holds.
243
- */
244
73
  get filter() {
245
74
  return this.filterState ?? this.defaultFilter;
246
75
  }
@@ -249,13 +78,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
249
78
  this.filterState = next;
250
79
  this.requestUpdate('filter', previous);
251
80
  }
252
- /**
253
- * The page on show, from 1.
254
- *
255
- * Clamped on the way out, so what this reports is the page it drew: a
256
- * bookmarked link to page 99 of a result that has since shrunk reads back as
257
- * the last page rather than as 99.
258
- */
259
81
  get page() {
260
82
  const asked = this.pageState ?? this.defaultPage;
261
83
  return Math.min(this.pages, Math.max(1, Math.floor(asked) || 1));
@@ -265,15 +87,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
265
87
  this.pageState = next;
266
88
  this.requestUpdate('page', previous);
267
89
  }
268
- /**
269
- * How many rows a page holds. Zero is every row, which is what "no paging" means.
270
- *
271
- * **It announces nothing, and that is not an omission.** Nothing in this
272
- * element ever asks for a different page size: a size selector is
273
- * `helix-pagination-size`, which a consumer puts in the `footer` slot and
274
- * answers themselves. Routing this through `requestChange` would announce an
275
- * event the element never has cause to fire.
276
- */
277
90
  get pageSize() {
278
91
  return Math.max(0, Math.floor(this.pageSizeState ?? this.defaultPageSize) || 0);
279
92
  }
@@ -282,14 +95,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
282
95
  this.pageSizeState = next;
283
96
  this.requestUpdate('pageSize', previous);
284
97
  }
285
- /**
286
- * Whether to draw a pagination under the grid.
287
- *
288
- * On by default once a page size is set, because a paged table with no way to
289
- * change pages shows a reader the first twenty rows and no way to know there
290
- * are more. Off for a consumer supplying their own through the `footer` slot,
291
- * which is why the slot replaces this rather than sitting beside it.
292
- */
293
98
  get paginated() {
294
99
  return this.paginatedState ?? this.pageSize > 0;
295
100
  }
@@ -298,56 +103,19 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
298
103
  this.paginatedState = next;
299
104
  this.requestUpdate('paginated', previous);
300
105
  }
301
- /** The columns declared as children, in the order they are written. */
302
106
  get tableColumns() {
303
- /* `?? []` is the server. Lit's element shim carries attributes and not a
304
- tree, so `children` is undefined there, and this getter is read during
305
- `render`: spreading it throws and takes the whole SSR suite with it. */
306
107
  return [...(this.children ?? [])].filter((child) => 'isTableColumn' in child && child.isTableColumn === true);
307
108
  }
308
- /** The columns reduced to what the ordering needs. */
309
109
  get axes() {
310
110
  return this.tableColumns
311
111
  .filter((column) => column.field !== '')
312
112
  .map((column) => ({ field: column.field, order: (row) => column.order(row) }));
313
113
  }
314
- /** The track list, from the columns if there are any. */
315
114
  get template() {
316
115
  const declared = this.tableColumns;
317
116
  return declared.length > 0 ? declared.map((column) => column.track).join(' ') : '';
318
117
  }
319
- /**
320
- * How many rows this draws before it is worth saying something, and why 2,000.
321
- *
322
- * Measured 28 Aug 2026 on a four-column grid, mounting and painting: 100 rows
323
- * in 10.9ms, 500 in 30.2ms, 2,000 in 83.2ms and **5,000 rows as 20,000 cells
324
- * in 186ms**. It is linear at roughly 37 microseconds a row, so the 100ms an
325
- * interaction has to stay inside lands near 2,700 and this sits under it.
326
- * Sorting stays cheap throughout, 23.7ms at five thousand, so the cost is the
327
- * cells rather than the work over them.
328
- */
329
118
  static { this.COMFORTABLE = 2_000; }
330
- /**
331
- * Says so when a table is asked to draw more than it comfortably can.
332
- *
333
- * **The component renders every row it is given and defaults to doing so.**
334
- * `defaultPageSize` is `0`, so `paginated` is false unless a consumer asks,
335
- * which means the out-of-the-box arrangement is the unbounded one and nothing
336
- * about it says that a result of any size will simply be drawn.
337
- *
338
- * **Windowing was refused**, which is worth recording because it is the
339
- * obvious answer. This element already has two mechanisms: `paginated` with a
340
- * `page-size`, and `mode="server"` where `rows` is one page, `total` is the
341
- * whole result and `aria-rowcount` already reports the result rather than the
342
- * window, which is the hard half of virtualization done correctly. A third
343
- * would fight the first two, and virtualizing a grid means variable row
344
- * heights, a sticky header and keeping `aria-rowindex` honest against a result
345
- * the element is no longer holding. `VirtualizeController` exists for the case
346
- * where the rows are a flat list and this is not that.
347
- *
348
- * Warned once per element, in a browser only: a warning during a server render
349
- * is one nobody can act on and one that would print per request.
350
- */
351
119
  warnIfCrowded(drawn) {
352
120
  if (this.warnedAboutSize ||
353
121
  drawn <= HelixHeadlessDataTable_1.COMFORTABLE ||
@@ -360,14 +128,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
360
128
  'crosses an interaction budget around 2,700. Set `page-size` to paginate, or `mode="server"` ' +
361
129
  'to hand it one page at a time with `total` describing the whole result.');
362
130
  }
363
- /**
364
- * The rows to draw, and the counts around them.
365
- *
366
- * In `server` mode this element applies nothing: `rows` is already the page,
367
- * `total` is the result, and both counts come from the consumer. Anything else
368
- * would be this element filtering a page it was handed, which narrows one
369
- * screen and calls it a search.
370
- */
371
131
  get view() {
372
132
  if (this.mode === 'server') {
373
133
  const total = Math.max(this.total, this.rows.length);
@@ -382,7 +142,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
382
142
  this.warnIfCrowded(view.rows.length);
383
143
  return view;
384
144
  }
385
- /** How many pages the result makes, which is at least one even when empty. */
386
145
  get pages() {
387
146
  const size = this.pageSize;
388
147
  if (size <= 0) {
@@ -391,12 +150,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
391
150
  const count = this.mode === 'server' ? Math.max(this.total, this.rows.length) : this.matched;
392
151
  return Math.max(1, Math.ceil(count / size));
393
152
  }
394
- /**
395
- * Rows after filtering, computed without paging.
396
- *
397
- * Separate from `view` because `pages` needs it and `page` needs `pages`:
398
- * asking `view` for it would recurse through the clamp in `page`.
399
- */
400
153
  get matched() {
401
154
  const query = this.filter.trim();
402
155
  if (query === '') {
@@ -411,17 +164,9 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
411
164
  : String(value).toLocaleLowerCase().includes(needle);
412
165
  })).length;
413
166
  }
414
- /** Emptiness is a fact about the data here, not about slotted children. */
415
167
  get hasRows() {
416
168
  return this.view.rows.length > 0;
417
169
  }
418
- /**
419
- * Asks for a different order, and reports whether the request stood.
420
- *
421
- * Announced before it is applied, so `preventDefault()` is a refusal and the
422
- * order does not move, which is what makes a server-side sort expressible: the
423
- * consumer refuses the local reorder, fetches, and hands back a new set.
424
- */
425
170
  requestSort(next) {
426
171
  return requestChange({
427
172
  apply: (value) => {
@@ -434,7 +179,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
434
179
  next,
435
180
  });
436
181
  }
437
- /** Asks for a different query. */
438
182
  requestFilter(next) {
439
183
  return requestChange({
440
184
  apply: (value) => {
@@ -447,7 +191,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
447
191
  next,
448
192
  });
449
193
  }
450
- /** Asks for a different page. */
451
194
  requestPage(next) {
452
195
  if (next === this.page || next < 1 || next > this.pages) {
453
196
  return false;
@@ -463,30 +206,12 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
463
206
  next,
464
207
  });
465
208
  }
466
- /**
467
- * Narrows the rows, from a consumer's own search field.
468
- *
469
- * Public because the query lives above the table in every design that has one,
470
- * and a consumer wiring their own input needs somewhere to send it. Returns
471
- * whether the change stood, the same as every other request here.
472
- */
473
209
  search(query) {
474
210
  return this.requestFilter(query);
475
211
  }
476
- /**
477
- * Cycles the order by one column, which is what a click on its heading means.
478
- *
479
- * Ascending, descending, then the order the rows arrived in. Public for the
480
- * same reason `search` is: a consumer's own heading UI needs a way to ask.
481
- */
482
212
  toggleSort(field) {
483
213
  return this.requestSort(format(cycle(parse(this.sort), field)));
484
214
  }
485
- /**
486
- * The columns into the header row, everything else into the toolbar and footer.
487
- *
488
- * Manual assignment, so a consumer's own markup is never written to.
489
- */
490
215
  assign() {
491
216
  const root = this.renderRoot;
492
217
  if (!root?.querySelector) {
@@ -513,30 +238,10 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
513
238
  super.willUpdate(changed);
514
239
  hostAria(this.internals, { label: this.label || 'Table', role: 'grid' });
515
240
  }
516
- /**
517
- * Says on the toolbar and the footer whether anything is in them.
518
- *
519
- * **A stylesheet cannot ask.** `[part="toolbar"]:has(> slot:not(:has(*)))` is
520
- * two faults at once: `:has()` may not be nested inside `:has()`, so the
521
- * selector fails to parse and the whole rule is discarded; and
522
- * `slot:not(:has(*))` is as true of a filled slot as of an empty one, because
523
- * assigned nodes are not a slot's DOM children. Both verified in a browser.
524
- */
525
241
  markEmpty() {
526
242
  this.markRegion('toolbar', ['toolbar']);
527
- /* The footer is not empty while this element is drawing its own pagination
528
- into it. */
529
243
  this.markRegion('footer', ['footer'], '[part="pagination"]');
530
244
  }
531
- /**
532
- * One region, hidden when its slot holds nothing and nothing of ours fills it.
533
- *
534
- * `hidden` beside the `data-empty` mirror: `data-empty` is the hook the styled
535
- * sheet reads, and this tier ships no sheet, so an empty region left in the box
536
- * tree would be a gap a headless consumer cannot close without knowing to look
537
- * for it. The styled rule stays too, because that sheet sets `display` on these
538
- * parts and therefore outranks the UA's `[hidden]` one.
539
- */
540
245
  markRegion(part, slots, occupied) {
541
246
  const region = this.renderRoot?.querySelector(`[part="${part}"]`);
542
247
  const empty = !slots.some((name) => slotFilled(this.renderRoot?.querySelector(`slot[name="${name}"]`))) && !(occupied && this.renderRoot?.querySelector(occupied));
@@ -549,20 +254,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
549
254
  this.assign();
550
255
  this.announceSort();
551
256
  }
552
- /**
553
- * Tells each column which way it is ordered, if it is the one.
554
- *
555
- * **The table owns the sort and the column carries the semantics**, which is
556
- * the only arrangement that can be right: `role="columnheader"` is on the
557
- * column, so `aria-sort` has to be there too, and the value is a fact about
558
- * the table's `sort` rather than about any column. Pushed down here rather
559
- * than pulled up there, because a column asking its parent would have to know
560
- * what kind of parent it has, and it is slotted into two.
561
- *
562
- * In `updated` and after `assign`, so the order announced is the order the
563
- * rows are already in. Announced before the rows moved is the sort a reader is
564
- * told about being one they cannot see yet.
565
- */
566
257
  announceSort() {
567
258
  const current = parse(this.sort);
568
259
  for (const column of this.tableColumns) {
@@ -585,10 +276,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
585
276
  renderRows(rows) {
586
277
  const columns = this.tableColumns;
587
278
  const field = this.key;
588
- /* Keyed, so a sort moves a row's element rather than rewriting every cell
589
- in place: focus inside a cell survives and a transition stays attached to
590
- the row it belongs to. The index is the fallback and is correct only
591
- while nothing reorders, which is why `key` is worth setting. */
592
279
  return html `${repeat(rows, (row, index) => (field ? String(row[field] ?? index) : index), (row, index) => html `<div
593
280
  part="row"
594
281
  role="row"
@@ -597,14 +284,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
597
284
  ${columns.map((column) => this.renderCell(column, row))}
598
285
  </div>`)}`;
599
286
  }
600
- /**
601
- * A row's place in the whole result, counting the header as row 1.
602
- *
603
- * **The number describes the result and not the window**, which is the whole
604
- * reason a virtualized or paged grid needs `aria-rowindex` at all: a screen
605
- * reader on page three should hear row 41, and a grid whose indices restart at
606
- * every page tells a reader the result is twenty rows long however many it has.
607
- */
608
287
  rowIndex(within) {
609
288
  const size = this.pageSize;
610
289
  const offset = size > 0 ? (this.page - 1) * size : 0;
@@ -642,21 +321,6 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
642
321
  : nothing}
643
322
  </div>`;
644
323
  }
645
- /**
646
- * Listens on the host, not on the slot the columns are assigned to.
647
- *
648
- * The columns are light-DOM children of this element, so their events bubble
649
- * to it directly and the shadow tree is not involved at all. Bound on the
650
- * `<slot>` instead, this works in a browser, because an event from a slotted
651
- * node passes through the slot in the flattened tree, and does not work in
652
- * happy-dom, which does not implement that propagation: measured, the host
653
- * hears the click and the slot does not. A spec written against the slot fails
654
- * in the suite while the component is correct.
655
- *
656
- * Added here rather than in the template because there is nothing in the
657
- * shadow tree to attach it to, and removed on disconnect because this element
658
- * is the listener's owner rather than its subject.
659
- */
660
324
  connectedCallback() {
661
325
  super.connectedCallback();
662
326
  this.addEventListener('click', this.handleHeadClick);
@@ -1,29 +1,3 @@
1
- /**
2
- * Filter, sort, then window. In that order, over the whole set, every time.
3
- *
4
- * **This is the file that stops the classic virtualization bug.** Sorting the
5
- * rendered window looks completely correct on the first screen and is wrong
6
- * everywhere else, and it is the natural thing to write once rows are data and
7
- * only some of them are drawn. Keeping the three operations here, as functions
8
- * over an array with no knowledge of what is on screen, is what makes that
9
- * mistake hard to make: nothing in this module can see a window, and `window()`
10
- * is the last thing applied rather than the first.
11
- *
12
- * The order is not a preference. Filtering after sorting sorts rows that are
13
- * about to be thrown away, which is only slower; paging before either is the bug.
14
- * Filter narrows, sort arranges what is left, the page is a slice of that, and
15
- * the count a reader is shown is the length after filtering rather than the
16
- * length of the data, or the pagination promises pages that are empty.
17
- */
18
- /**
19
- * `field:asc` and `field:desc`, and nothing else.
20
- *
21
- * A bare `field` parses as ascending, because that is what a URL query usually
22
- * carries and refusing it would make the common case the verbose one. Anything
23
- * unparseable is no sort rather than an error: this value arrives from a query
24
- * string as often as from code, and a bad one should leave the table in the
25
- * order the data came in rather than break the page.
26
- */
27
1
  export const parse = (spec) => {
28
2
  const trimmed = spec.trim();
29
3
  if (trimmed === '') {
@@ -35,36 +9,13 @@ export const parse = (spec) => {
35
9
  }
36
10
  return { direction, field };
37
11
  };
38
- /** A `Sort` written back out, so the property and the attribute agree. */
39
12
  export const format = (sort) => sort ? `${sort.field}:${sort.direction}` : '';
40
- /**
41
- * What a reader gets on the next click of a sortable heading.
42
- *
43
- * Ascending, descending, then none. **The third state is not an omission to be
44
- * tidied away**: the order rows arrived in is information the data came with,
45
- * often the server's own relevance ranking, and a two-state cycle makes it
46
- * unreachable once a reader has sorted once.
47
- */
48
13
  export const cycle = (current, field) => {
49
14
  if (current?.field !== field) {
50
15
  return { direction: 'asc', field };
51
16
  }
52
17
  return current.direction === 'asc' ? { direction: 'desc', field } : undefined;
53
18
  };
54
- /**
55
- * Rows matching a query, across every axis.
56
- *
57
- * One query over all columns rather than a predicate per column, which is what a
58
- * reader means by a search box above a table. Per-column predicates are a
59
- * different feature and compose with this rather than replacing it.
60
- *
61
- * Case-insensitive and substring, on the ordering value rather than the rendered
62
- * one: a cell showing a formatted date should be findable by the text a reader
63
- * sees, and a cell showing an icon should be findable by the value behind it.
64
- * Where those differ, `compare` on the column is what decides, which is the same
65
- * function sorting uses, so search and sort cannot disagree about what a column
66
- * holds.
67
- */
68
19
  export const filter = (rows, query, axes) => {
69
20
  const needle = query.trim().toLocaleLowerCase();
70
21
  if (needle === '') {
@@ -72,16 +23,6 @@ export const filter = (rows, query, axes) => {
72
23
  }
73
24
  return rows.filter((row) => axes.some((axis) => text(axis.order(row)).toLocaleLowerCase().includes(needle)));
74
25
  };
75
- /**
76
- * Rows in the asked-for order, stably.
77
- *
78
- * **Stable, and that is load-bearing rather than tidy.** `Array.prototype.sort`
79
- * is required to be stable since ES2019, so equal rows keep the order they
80
- * arrived in, which means sorting by one column and then another leaves the
81
- * first as the tiebreak. A reader who sorts by status and then by name expects
82
- * exactly that, and an unstable sort would shuffle equal rows on every
83
- * re-render, which reads as the table twitching.
84
- */
85
26
  export const sort = (rows, spec, axes) => {
86
27
  const axis = spec ? axes.find((one) => one.field === spec.field) : undefined;
87
28
  if (!spec || !axis) {
@@ -90,14 +31,6 @@ export const sort = (rows, spec, axes) => {
90
31
  const sign = spec.direction === 'asc' ? 1 : -1;
91
32
  return [...rows].sort((left, right) => compare(axis.order(left), axis.order(right), sign));
92
33
  };
93
- /**
94
- * One page of rows, clamped.
95
- *
96
- * A page past the end returns the last page rather than nothing, because the
97
- * usual way to be past the end is a bookmarked link after the result shrank, and
98
- * an empty table is a worse answer than the last page of a smaller one. The
99
- * same clamp `helix-pagination` already applies to what it reports.
100
- */
101
34
  export const window_ = (rows, page, size) => {
102
35
  if (size <= 0) {
103
36
  return [...rows];
@@ -107,12 +40,6 @@ export const window_ = (rows, page, size) => {
107
40
  const from = (wanted - 1) * size;
108
41
  return rows.slice(from, from + size);
109
42
  };
110
- /**
111
- * A value as text, for matching.
112
- *
113
- * `null` and `undefined` become empty rather than their own names, so a search
114
- * for "null" does not match every hole in the data.
115
- */
116
43
  const text = (value) => {
117
44
  if (value === null || value === undefined) {
118
45
  return '';
@@ -122,26 +49,6 @@ const text = (value) => {
122
49
  }
123
50
  return String(value);
124
51
  };
125
- /**
126
- * Two cell values ordered, with the types a table actually holds.
127
- *
128
- * **Numbers compare as numbers and strings collate.** `String(a) < String(b)`
129
- * puts 10 before 9 and puts every accented word after every unaccented one, and
130
- * both look like the sort is broken while every value in the column is correct.
131
- * `localeCompare` handles the second, and the numeric branch handles the first.
132
- *
133
- * **Holes sort last in both directions**, which is deliberate and is the one
134
- * choice here a reader could disagree with. A missing value is not smaller than
135
- * a present one, it is absent, and burying the holes at the end is what a reader
136
- * scanning for the largest or smallest actual value wants. Reversing them with
137
- * the direction puts every hole first on a descending sort, which is the least
138
- * useful screen the data can produce.
139
- *
140
- * **`sign` is applied in here rather than to the result**, and that is why it is
141
- * a parameter rather than the caller's multiplication. Written the obvious way,
142
- * as `sign * compare(a, b)`, the constant this returns for a hole flips with the
143
- * direction and the holes lead a descending sort.
144
- */
145
52
  const compare = (left, right, sign) => {
146
53
  const leftMissing = left === null || left === undefined || left === '';
147
54
  const rightMissing = right === null || right === undefined || right === '';
@@ -159,13 +66,6 @@ const compare = (left, right, sign) => {
159
66
  }
160
67
  return (sign * text(left).localeCompare(text(right), undefined, { numeric: true, sensitivity: 'base' }));
161
68
  };
162
- /**
163
- * The whole pass, in the one order that is correct.
164
- *
165
- * Exported as a single function so a caller cannot get the order wrong by
166
- * calling the three in the sequence that reads naturally, which is the sequence
167
- * that pages before it sorts.
168
- */
169
69
  export const ordered = (rows, axes, { page, query, size, spec }) => {
170
70
  const matched = filter(rows, query, axes);
171
71
  const arranged = sort(matched, parse(spec), axes);