@fusion.dev/helix-headless 0.2.0 → 0.4.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,285 +9,37 @@ import { caretDoubleLeft, caretDoubleRight, caretLeft, caretRight, } from '@fusi
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
- /**
13
- * Fills `{page}` and `{pages}` in a template a consumer wrote.
14
- *
15
- * A template rather than a prefix, because word order is not universal and a
16
- * `page-label="Page"` that this glued a number onto would produce the English
17
- * order in every language. `href="/products?page={page}"` is the same mechanism
18
- * doing a second job, which is why there is one placeholder syntax and not two.
19
- *
20
- * `format` is what makes the numbers local as well as the words. It is the
21
- * identity for an address and an `Intl.NumberFormat` for anything a person
22
- * reads: a URL is machine-facing and `?page=1,234` is a different page from
23
- * `?page=1234`, where a label read aloud should say the number the way the
24
- * reader's locale says it.
25
- */
26
12
  const fill = (template, page, pages, format = String) => template.replaceAll('{page}', format(page)).replaceAll('{pages}', format(pages));
27
- /**
28
- * @summary Moves between pages of a longer result.
29
- *
30
- * @description
31
- * The component is the truncation. Given a page and a total it works out which
32
- * numbers to show and where the runs break, which is the whole of what a
33
- * consumer would otherwise write by hand and get subtly wrong; everything else
34
- * here is markup around that. So this is data-driven rather than composed:
35
- * `<helix-pagination page="4" pages="12">` rather than twelve slotted children
36
- * a consumer has to keep in step with their own query.
37
- *
38
- * **The width does not change as you page.** The run around the current page is
39
- * `siblings` either side normally, and grows to absorb a gap slot when it meets
40
- * a boundary, so the control holds the same number of slots from the first page
41
- * to the last. Every arrangement that lets the count vary makes the buttons move
42
- * under the pointer between one click and the next.
43
- *
44
- * **A gap is only drawn where it hides more than one page.** An ellipsis
45
- * standing in for a single number costs the same space as the number and tells
46
- * the reader less.
47
- *
48
- * **And a gap is a control.** It goes to the middle of the run it stands over,
49
- * which is the binary-search step: any page in a set of any size is a handful of
50
- * presses, where a jump of a fixed size would need forty of them on a set of a
51
- * hundred and twenty-eight. A menu listing the hidden pages was the other idea
52
- * and the arithmetic refuses it, for the reasons on `renderGap`.
53
- *
54
- * **`href` is what makes these links.** Set it and every page renders an `<a>`
55
- * with `{page}` filled in, so the server's output is crawlable, a middle click
56
- * opens a tab, and the address bar is the state. Left unset they are buttons,
57
- * which is right for a table paging in place. Either way the click announces
58
- * `helix-navigate` first, and `preventDefault()` on it stops the navigation as
59
- * well as the change: that is how a router takes over without the page reloading
60
- * underneath it.
61
- *
62
- * **`helix-navigate` and not `helix-change`**, for the reason the calendar pages
63
- * with it too. Moving to page four chooses nothing; it moves the view over a
64
- * result that has not changed. A consumer driving the view has to be able to
65
- * hear that without being told an answer changed.
66
- *
67
- * **The count and the page-size control are their own elements.** A slot can
68
- * only offer one place: at one end of one row, in one order, on one axis.
69
- * `Pagination.Summary` and `Pagination.Size` sit beside this one instead, so
70
- * the direction and the order of a footer are the consumer's, which is what
71
- * they were always going to want. `total` and `page-size` stay here for the
72
- * arithmetic below and nothing else.
73
- *
74
- * **A single page renders nothing.** A control offering one page offers no
75
- * navigation, and the row it sits in should close over it rather than hold a
76
- * space for a thing that cannot be used. Reported through
77
- * `ElementInternals.states` rather than an attribute on the host, because an
78
- * attribute written at upgrade is markup a server's output never had and a
79
- * framework reports the difference; a custom state carries the same fact to a
80
- * stylesheet and none of that to the DOM.
81
- *
82
- * **And a page change is announced.** Moving the page moves `aria-current` onto
83
- * a different element, which a screen reader does not announce, so a reader who
84
- * pressed 5 hears the word "5" from the button they were on and nothing at all
85
- * about having arrived. A polite live region says where they now are. It ships
86
- * empty and is filled only on a change, because a region populated as it enters
87
- * the document announces itself on arrival.
88
- *
89
- * `page` is what the element holds and `default-page` seeds it. A consumer
90
- * paging through a router refuses `helix-navigate` and assigns their own, which
91
- * is how they take the decision.
92
- *
93
- * @since 0.1.0
94
- * @status experimental
95
- * @category navigation
96
- * @aliases Pager, Page nav
97
- * @documentation https://helix.fusion.dev/components/pagination
98
- *
99
- * @slot first-icon - The glyph on the jump-to-first control, usually a double caret.
100
- * @slot last-icon - The glyph on the jump-to-last control, usually a double caret.
101
- * @slot next-icon - The glyph on the forward-one-page control, usually a caret.
102
- * @slot previous-icon - The glyph on the back-one-page control, usually a caret.
103
- *
104
- * @event {HelixNavigateEvent} helix-navigate - Fired before the page changes, and cancelable. The only way to refuse one.
105
- *
106
- * @csspart first - The control that jumps to the first page, under `ends`.
107
- * @csspart gap - The control standing over pages not shown, which jumps into them.
108
- * @csspart item - One slot in the run, holding a page or a gap.
109
- * @csspart last - The control that jumps to the last page, under `ends`.
110
- * @csspart list - The run of pages.
111
- * @csspart live - The region that says the page changed. Clipped, not hidden.
112
- * @csspart mark - The glyph on a first, previous, next or last control, where none is slotted.
113
- * @csspart next - The control that moves forward one page.
114
- * @csspart page - The control for one page.
115
- * @csspart previous - The control that moves back one page.
116
- * @csspart status - The reading of where you are, in `compact`.
117
- */
118
13
  let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement {
119
14
  constructor() {
120
15
  super(...arguments);
121
- /**
122
- * How many pages to show at each end, whatever the current one is.
123
- */
124
16
  this.boundaries = 1;
125
- /**
126
- * Where the numbers are replaced by a reading of the two.
127
- *
128
- * A different structure rather than a different look, which is why it is
129
- * here: the run of pages is gone and a single status line stands in its
130
- * place. This is the shape a narrow screen wants, and the only shape possible
131
- * where the total is large enough that no window over it is useful.
132
- *
133
- * Not reflected, for the reason `ends` is not: it decides what renders, so
134
- * nothing has to select on it.
135
- */
136
17
  this.compact = false;
137
- /**
138
- * Page to open on, where nothing declared one.
139
- */
140
18
  this.defaultPage = 1;
141
- /**
142
- * Whether to offer a jump to the first and last pages.
143
- *
144
- * Off by default. The gaps already reach any page in a few presses and the
145
- * last page is one of the two the run always shows, so on a short set these
146
- * are two more controls for a move already on screen. They earn their place
147
- * where the reader is deep in a long result and "back to the start" is the
148
- * move they actually want.
149
- *
150
- * Not reflected. It adds two elements rather than changing how any of them
151
- * look, so no rule selects on it, and the house rule is that reflection
152
- * exists for selectors: an unread reflected attribute is one more thing
153
- * written onto a host at upgrade that a server's output never had.
154
- */
155
19
  this.ends = false;
156
- /**
157
- * Accessible name for the control that jumps to the first page.
158
- */
159
20
  this.firstLabel = 'First page';
160
- /**
161
- * Accessible name for a gap, with `{page}` filled in as where it goes.
162
- */
163
21
  this.gapLabel = 'Skip to page {page}';
164
- /**
165
- * Address for a page, with `{page}` standing in for its number.
166
- *
167
- * Absent renders buttons. Present renders links, which is what a paged result
168
- * with its own URL should be: a crawler follows them, a middle click opens
169
- * one, and the back button works without the consumer wiring anything.
170
- */
171
22
  this.href = '';
172
- /**
173
- * Accessible name for the navigation landmark.
174
- */
175
23
  this.label = 'Pagination';
176
- /**
177
- * Accessible name for the control that jumps to the last page.
178
- */
179
24
  this.lastLabel = 'Last page';
180
- /**
181
- * Locale for the page numbers. Absent uses the runtime's own.
182
- *
183
- * The same property the calendar takes, doing the same job one layer down:
184
- * the templates make the *words* the consumer's, and this makes the *numbers*
185
- * theirs. A page number is a number like any other, so `1234` is `1,234` in
186
- * `en-US`, `1.234` in `de-DE` and `١٢٣٤` in `ar-EG`, and a component that
187
- * lets a consumer translate "Page {page} of {pages}" and then prints Latin
188
- * digits into it has done half the job.
189
- *
190
- * Not applied to `href`. That one is machine-facing, and `?page=1,234` is a
191
- * different address from `?page=1234`.
192
- */
193
25
  this.locale = '';
194
- /**
195
- * Accessible name for the control that moves forward.
196
- */
197
26
  this.nextLabel = 'Next page';
198
- /**
199
- * Accessible name for one page, with `{page}` and `{pages}` filled in.
200
- */
201
27
  this.pageLabel = 'Page {page}';
202
- /**
203
- * How many pages there are in total.
204
- *
205
- * Clamped to at least one wherever it is read, so a result that has not
206
- * loaded yet renders a single page rather than an empty control that then
207
- * appears and pushes the layout down.
208
- *
209
- * Left unset where `total` and `page-size` are both given, since the two of
210
- * them already say it. A fallback rather than an override: writing this wins,
211
- * so there is never a question of which of two sources is current.
212
- */
213
28
  this.pages = 0;
214
- /**
215
- * How many items are on a page.
216
- *
217
- * Only arithmetic: this does not slice anything, because the component never
218
- * sees the items. It is what turns `total` into a page count, and nothing
219
- * else.
220
- */
221
29
  this.pageSize = 0;
222
- /**
223
- * Accessible name for the control that moves back.
224
- */
225
30
  this.previousLabel = 'Previous page';
226
- /**
227
- * How many pages to show either side of the current one.
228
- */
229
31
  this.siblings = 1;
230
- /**
231
- * The reading shown in `compact`, with `{page}` and `{pages}` filled in.
232
- *
233
- * No noun: the landmark this sits in is already named "Pagination", so
234
- * "Page 7 of 24" says it twice to a screen reader and takes a third of the
235
- * width from a control whose whole reason for existing is that the row is
236
- * narrow. The numbers are the reading.
237
- */
238
32
  this.statusLabel = '{page} of {pages}';
239
- /**
240
- * How many items there are across every page.
241
- *
242
- * What `pages` falls back to together with `page-size`. Unset is the common
243
- * case, where a consumer knows the page total and not the item total.
244
- *
245
- * Nothing is rendered from it. `Pagination.Summary` is what reads a total out
246
- * to a reader, and takes its own.
247
- */
248
33
  this.total = 0;
249
- /**
250
- * What the live region says, and nothing until the page has moved once.
251
- *
252
- * `pageLabel` rather than a template of its own, so the announcement is the
253
- * same sentence the control the reader just pressed already carried, and
254
- * there is not a ninth string to translate for the sake of one of them.
255
- */
256
34
  this.announcement = '';
257
- /** The page last announced, so the first render announces nothing. */
258
35
  this.announced = undefined;
259
- /** The page on show, whoever set it, and `undefined` until somebody has. */
260
36
  this.pageState = undefined;
261
- /**
262
- * The click handler, which is also what stops a link from following itself.
263
- *
264
- * A refused change has to refuse the navigation with it, or a router that
265
- * cancels the event still loses the document underneath it. `preventDefault`
266
- * on a button's click does nothing and costs nothing, so this is one handler
267
- * for both shapes rather than two that have to stay in step.
268
- */
269
37
  this.go = (next, event) => {
270
38
  if (!this.request(next)) {
271
39
  event.preventDefault();
272
40
  }
273
41
  };
274
42
  }
275
- /**
276
- * The page on show, one-based.
277
- *
278
- * An accessor pair, the same shape as the calendar's `month`. Reading gives
279
- * the page on show; writing sets it, and `default-page` seeds one the element
280
- * keeps until somebody does.
281
- *
282
- * **Clamped on the way out, because "the page on show" is what it says it
283
- * gives.** Returning whatever was written means `default-page="99"` on a
284
- * ten-page result renders page 10, marks page 10 with `aria-current`, and
285
- * reports 99, so a consumer syncing their query from this reads a page that
286
- * does not exist while the control in front of them says otherwise. Not a
287
- * far-fetched value: it is a bookmarked link after the result shrank.
288
- *
289
- * Rounded here too, so `2.7` reads as the 2 it renders.
290
- */
291
43
  get page() {
292
44
  const asked = this.pageState ?? this.defaultPage;
293
45
  return Math.min(this.count, Math.max(1, Math.floor(asked) || 1));
@@ -297,19 +49,9 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
297
49
  this.pageState = next;
298
50
  this.requestUpdate('page', previous);
299
51
  }
300
- /** Held across every component asking for the same locale. */
301
52
  get numbers() {
302
53
  return formatNumbers(this.locale);
303
54
  }
304
- /**
305
- * How many pages, clamped once, so every reader agrees on it.
306
- *
307
- * `pages` first, because writing it is the explicit answer. Then `total` over
308
- * `pageSize`, which is the same fact stated the other way round and is what a
309
- * consumer holding a result set already has. Then one, so a result that has
310
- * not loaded renders a single page rather than an empty control that appears
311
- * later and pushes the layout down.
312
- */
313
55
  get count() {
314
56
  if (this.pages > 0) {
315
57
  return Math.max(1, Math.floor(this.pages));
@@ -319,23 +61,9 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
319
61
  }
320
62
  return 1;
321
63
  }
322
- /** The page on show, which `page` has already clamped. */
323
64
  get current() {
324
65
  return this.page;
325
66
  }
326
- /**
327
- * Which pages to show, and where the runs break.
328
- *
329
- * `most` is the number of slots the control holds at its widest: both
330
- * boundaries, the run around the current page, and a gap on each side. Below
331
- * that there is nothing to hide, and the whole range is shown.
332
- *
333
- * Above it, the two gaps are decided independently, and a gap standing over
334
- * exactly one page is not drawn: `boundaries + 2` is the first page whose
335
- * predecessor is not a boundary, so a run starting there is contiguous with
336
- * what precedes it. Where one side has no gap the run on that side extends to
337
- * `most` slots, which is what holds the width still.
338
- */
339
67
  get slots() {
340
68
  const total = this.count;
341
69
  const current = this.current;
@@ -349,8 +77,6 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
349
77
  }
350
78
  const before = current - siblings > boundaries + 2;
351
79
  const after = current + siblings < total - boundaries - 1;
352
- /* A gap carries the run it stands over rather than being a bare marker,
353
- because it is a control now and a control has to say where it goes. */
354
80
  const gap = (from, to) => ({ from, to });
355
81
  if (!before) {
356
82
  const head = most - boundaries - 1;
@@ -372,13 +98,6 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
372
98
  ...range(total - boundaries + 1, total),
373
99
  ];
374
100
  }
375
- /**
376
- * Asks to move, and reports whether the request stood.
377
- *
378
- * Announced before it is applied, so `preventDefault()` is a refusal and
379
- * nothing moves. A consumer who holds the page refuses it and assigns their
380
- * own.
381
- */
382
101
  request(next) {
383
102
  if (next === this.current || next < 1 || next > this.count) {
384
103
  return false;
@@ -396,28 +115,12 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
396
115
  }
397
116
  willUpdate(changed) {
398
117
  super.willUpdate(changed);
399
- /* On the host through internals, so the landmark is the element itself. A
400
- page may hold more than one navigation and landmarks are how a
401
- screen-reader user chooses between them, which is why this carries a
402
- default name at all: an author's own `aria-label` is an attribute and
403
- beats internals, so naming it here costs them nothing. */
404
118
  hostAria(this.internals, { label: this.label, role: 'navigation' });
405
- /* Through `toggleState`, which carries the feature test: an engine without
406
- `CustomStateSet` gets a control that renders its one page instead of
407
- disappearing, which is degraded rather than broken. */
408
119
  this.toggleState('empty', this.count <= 1);
409
120
  }
410
- /**
411
- * Says where the reader now is, once they have moved.
412
- *
413
- * In `updated` rather than in `request`, so it covers both modes: a controlled
414
- * consumer assigns `page` and never goes through the request path at all, and
415
- * a reader who got there that way deserves the same sentence.
416
- */
417
121
  updated(changed) {
418
122
  super.updated(changed);
419
123
  if (this.announced === undefined) {
420
- // The first render establishes where we started. Arriving is not moving.
421
124
  this.announced = this.page;
422
125
  return;
423
126
  }
@@ -426,13 +129,6 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
426
129
  this.announcement = fill(this.pageLabel, this.page, this.count, this.numbers);
427
130
  }
428
131
  }
429
- /**
430
- * One page, as a link or a button.
431
- *
432
- * `aria-current="page"` is what says which one you are on, and it stays a
433
- * control rather than becoming text: a reader who tabs into the run and finds
434
- * the current page missing from it has lost the only landmark in the row.
435
- */
436
132
  renderPage(page) {
437
133
  const current = page === this.current;
438
134
  const shown = this.numbers(page);
@@ -458,27 +154,6 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
458
154
  </button>`}
459
155
  </li>`;
460
156
  }
461
- /**
462
- * A gap, which is a control and not a mark.
463
- *
464
- * **It goes to the middle of what it hides**, and the label says so. That is
465
- * the binary-search step, so an arbitrary page in a set of any size is a
466
- * handful of presses: 128 pages is `1 … 96 97 98 … 128`, and the leading gap
467
- * lands on 48, whose gaps land on 24 and 88, and so on. A jump of a fixed size
468
- * would need forty presses on that set.
469
- *
470
- * **A menu over these pages was the other idea and it does not survive the
471
- * arithmetic.** The run a gap covers is unbounded by construction: two pages
472
- * on a twelve-page set, ninety-four on a hundred-and-twenty-eight-page one. A
473
- * menu is pleasant at the first and is a scroll of ninety-four items at the
474
- * second, which is worse than the ellipsis it replaced, and there is no total
475
- * at which it stops being possible. It would also make `Pagination` depend on
476
- * `Menu` and `Popover` for a case a button already covers.
477
- *
478
- * So it is announced rather than hidden: a control a reader can reach has to
479
- * be in the tree, and the label naming its destination is the whole
480
- * affordance.
481
- */
482
157
  renderGap(hidden) {
483
158
  const to = Math.round((hidden.from + hidden.to) / 2);
484
159
  const name = fill(this.gapLabel, to, this.count, this.numbers);
@@ -501,15 +176,6 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
501
176
  </button>`}
502
177
  </li>`;
503
178
  }
504
- /**
505
- * Everything a step needs, computed once, so the markup below is only markup.
506
- *
507
- * Split out because the part name has to be a literal. A part name is read
508
- * from the source, so a computed one is a part nothing can see: nothing pairs it with its `@csspart` and a later rename takes
509
- * the promise silently. With four controls and two shapes each that is eight
510
- * literals, and eight copies of the arithmetic behind them would be the
511
- * actual duplication. This is the arithmetic, written once.
512
- */
513
179
  step(to, spent, icon, name) {
514
180
  return {
515
181
  address: spent ? nothing : fill(this.href, to, this.count),
@@ -522,26 +188,6 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
522
188
  spent,
523
189
  };
524
190
  }
525
- /**
526
- * The four controls, inert at the end each points at.
527
- *
528
- * Inert rather than absent: a control that disappears at the first page takes
529
- * the row's width with it and moves everything else under the pointer. In
530
- * link mode the inert one is an `<a>` with no `href`, which is not a link and
531
- * is not focusable, and is the platform's own way of saying there is nowhere
532
- * to go.
533
- *
534
- * **Each glyph is a slot fallback, and each method is a seam**, which answer
535
- * two different people: the slot is how a page swaps a caret for its own icon
536
- * set, and `protected` is how the tier above swaps all four without
537
- * reimplementing a template that renders eight controls across two shapes.
538
- * `Select` and `Listbox` carry the same pairing on one mark each.
539
- *
540
- * **Four slot names rather than one**, even though the four controls draw
541
- * from the same shared `mark` part. A single `<slot name="icon">` is assigned
542
- * once and the other three occurrences of it get nothing, so a consumer
543
- * replacing the caret would silently lose three of their four marks.
544
- */
545
191
  renderFirst() {
546
192
  const { address, click, glyph, name, off, spent } = this.step(1, this.current <= 1, caretDoubleLeft, this.firstLabel);
547
193
  const mark = html `<slot name="first-icon">${glyph}</slot>`;
@@ -611,13 +257,6 @@ let HelixHeadlessPagination = class HelixHeadlessPagination extends HelixElement
611
257
  </ol>
612
258
  ${this.renderNext()}${this.ends ? this.renderLast() : nothing}${this.renderLive()}`;
613
259
  }
614
- /**
615
- * The region that says the page moved.
616
- *
617
- * Always rendered and empty until it has something to say. A live region
618
- * populated at the moment it enters the document announces itself on arrival,
619
- * which for a component that upgrades on load is every page load.
620
- */
621
260
  renderLive() {
622
261
  return html `<span part="live" role="status" aria-live="polite">${this.announcement}</span>`;
623
262
  }
@@ -8,87 +8,17 @@ import { customElement, formatNumbers, HelixBeforeChangeEvent, HelixElement, req
8
8
  import { nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
- /* Side-effect imports, because this renders a select and an option in its own
12
- shadow root and a custom element registers on import. The paths are relative,
13
- so this registers the base pair and the styled file's identical imports
14
- register the styled pair; `tier` picks which names the template asks for. */
15
11
  import '../../option/index.js';
16
12
  import '../../select/index.js';
17
- /**
18
- * @summary Chooses how many items a page holds.
19
- *
20
- * @description
21
- * Its own element beside `Pagination` rather than a slot inside it, for the
22
- * reason `Pagination.Summary` is: a footer's direction and order are the
23
- * consumer's, and a control that can only sit at one end of one row has taken
24
- * that decision for them.
25
- *
26
- * **`Select`, not a native `<select>`.** Two dropdowns identical closed and
27
- * different open meet each other in the same footer as a table's filters. The
28
- * caret is `Select`'s rather than a color literal baked into a data URI,
29
- * because a background image cannot read `currentColor`, and the label is a
30
- * real `<label for>` against the trigger's own id.
31
- *
32
- * **The label sits on the row, not above it.** `Select`'s host is a column, and
33
- * an outer-tree rule beats a `:host` rule, so turning it into a row needs no
34
- * property on `Select` and nothing written into the consumer's markup.
35
- *
36
- * **An empty `sizes` renders nothing**, rather than falling back to the
37
- * built-in list, which would be a component overruling a consumer who said
38
- * "none". Same rule as `Pagination` over a single page, and reported the same
39
- * way, through `ElementInternals.states` rather than an attribute the server
40
- * never wrote.
41
- *
42
- * `value` is the controlled property and `default-value` seeds one this owns.
43
- * Changing it announces `helix-before-change`; re-querying and moving back to
44
- * page one is the consumer's, because only they know whether the reader's
45
- * position should survive it.
46
- *
47
- * @since 0.1.0
48
- * @status experimental
49
- * @aliases Page size, Rows per page
50
- * @documentation https://helix.fusion.dev/components/pagination
51
- *
52
- * @event {HelixBeforeChangeEvent} helix-before-change - Fired before the size changes, and cancelable. The only way to refuse one.
53
- *
54
- * @csspart control - The select.
55
- * @csspart label - The words beside the number, forwarded from the select.
56
- * @csspart trigger - The closed control, forwarded from the select.
57
- * @csspart value - The chosen size inside the trigger, forwarded from the select.
58
- */
59
13
  let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends HelixElement {
60
14
  constructor() {
61
15
  super(...arguments);
62
- /**
63
- * Size to start on, where nothing declared one.
64
- */
65
16
  this.defaultValue = 0;
66
- /**
67
- * The words beside the control.
68
- */
69
17
  this.label = 'Per page';
70
- /**
71
- * Locale for the numbers.
72
- *
73
- * Absent uses the runtime's own.
74
- */
75
18
  this.locale = '';
76
- /**
77
- * The sizes offered, comma separated.
78
- *
79
- * A string rather than an array, so the whole control is expressible in
80
- * markup and survives a server render. Anything that is not a positive number
81
- * is dropped rather than rendered as an option that cannot mean anything.
82
- */
83
19
  this.sizes = '10,25,50,100';
84
- /** The size, whoever set it, and `undefined` until somebody has. */
85
20
  this.valueState = undefined;
86
21
  this.handleChange = (event) => {
87
- /* Stopped, then re-announced under this element's own name. `Select`'s
88
- `helix-before-change` bubbles and is composed, so left alone it would cross this
89
- shadow boundary and be retargeted to look like this element's: a consumer
90
- would get two events per change, one of them carrying `Select`'s idea of
91
- what a name and a value are. */
92
22
  event.stopPropagation();
93
23
  const next = Number.parseInt(event.detail?.value ?? '', 10);
94
24
  if (!Number.isFinite(next) || next === this.current) {
@@ -104,13 +34,6 @@ let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends Heli
104
34
  host: this,
105
35
  next,
106
36
  });
107
- /* Put back where the consumer refused it or owns it, because the control has
108
- already moved: a select changes itself and then tells you, which is the
109
- one case `requestChange` cannot announce ahead of. Lit skips a
110
- property binding whose value has not changed, so a refusal leaves the
111
- select holding the reader's choice and nothing else would put it back.
112
- Deferred, so the read happens after the render a successful change asked
113
- for rather than racing it. */
114
37
  this.updateComplete.then(() => {
115
38
  const control = this.renderRoot?.querySelector('[part="control"]');
116
39
  if (control && control.value !== String(this.current)) {
@@ -119,13 +42,6 @@ let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends Heli
119
42
  });
120
43
  };
121
44
  }
122
- /**
123
- * The size in force.
124
- *
125
- * Reading gives the real one; writing declares it the consumer's, and
126
- * `default-value` seeds one this element owns. Same accessor pair as
127
- * `Pagination.page` and the calendar's `month`.
128
- */
129
45
  get value() {
130
46
  return this.valueState ?? this.defaultValue;
131
47
  }
@@ -134,40 +50,23 @@ let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends Heli
134
50
  this.valueState = next;
135
51
  this.requestUpdate('value', previous);
136
52
  }
137
- /** Held across every component asking for the same locale. */
138
53
  get numbers() {
139
54
  return formatNumbers(this.locale);
140
55
  }
141
- /** The sizes, parsed once, with anything that is not a size dropped. */
142
56
  get options() {
143
57
  return this.sizes
144
58
  .split(',')
145
59
  .map((one) => Number.parseInt(one.trim(), 10))
146
60
  .filter((one) => Number.isFinite(one) && one > 0);
147
61
  }
148
- /** The one in force, which is the first offered where nothing named one. */
149
62
  get current() {
150
63
  const options = this.options;
151
64
  return options.includes(this.value) ? this.value : (options[0] ?? 0);
152
65
  }
153
66
  willUpdate(changed) {
154
67
  super.willUpdate(changed);
155
- /* Through `toggleState`, like `Pagination`'s: an engine without
156
- `CustomStateSet` gets an empty select rather than no element. */
157
68
  this.toggleState('empty', this.options.length === 0);
158
69
  }
159
- /**
160
- * The control itself, which is the whole of this part, behind a seam the styled tier can move.
161
- *
162
- * A method rather than a fragment inline, so a tier above can render its own,
163
- * wrap this one, pass it a property the base does not know about, or leave it
164
- * out, without reimplementing the whole template to do any of them.
165
- *
166
- * **The tags are interpolated, so each tier renders its own.** Writing
167
- * `<helix-select>` out would make this tier compose an element it does not
168
- * ship and cannot test against, there being no `helix-select` here. `tier`
169
- * resolves the name instead, and the styled subclass changes it in one line.
170
- */
171
70
  renderSelect() {
172
71
  const select = this.tag('select');
173
72
  const option = this.tag('option');
@@ -185,9 +84,6 @@ let HelixHeadlessPaginationSize = class HelixHeadlessPaginationSize extends Heli
185
84
  if (this.options.length === 0) {
186
85
  return nothing;
187
86
  }
188
- /* `Select` renders the label and gives the trigger the id it points at, so
189
- the association is the platform's and is already covered by that
190
- component's own specs. Nothing here reconstructs it. */
191
87
  return html `${this.renderSelect()}`;
192
88
  }
193
89
  };