@limetech/lime-elements 40.4.2 → 40.5.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 (126) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/component-UyqdiWGc.js +1576 -0
  3. package/dist/cjs/lime-elements.cjs.js +1 -1
  4. package/dist/cjs/limel-action-bar_3.cjs.entry.js +1 -1
  5. package/dist/cjs/limel-ai-avatar.cjs.entry.js +1 -1
  6. package/dist/cjs/{limel-breadcrumbs_8.cjs.entry.js → limel-breadcrumbs_6.cjs.entry.js} +188 -2164
  7. package/dist/cjs/limel-callout.cjs.entry.js +1 -1
  8. package/dist/cjs/limel-chart.cjs.entry.js +1 -1
  9. package/dist/cjs/limel-chip-set.cjs.entry.js +1 -1
  10. package/dist/cjs/limel-chip_2.cjs.entry.js +1 -1
  11. package/dist/cjs/limel-code-diff.cjs.entry.js +1 -1
  12. package/dist/cjs/limel-code-editor.cjs.entry.js +1 -1
  13. package/dist/cjs/limel-collapsible-section.cjs.entry.js +1 -1
  14. package/dist/cjs/limel-drag-handle.cjs.entry.js +1 -1
  15. package/dist/cjs/limel-email-viewer.cjs.entry.js +1 -1
  16. package/dist/cjs/limel-file-viewer.cjs.entry.js +1 -1
  17. package/dist/cjs/limel-file.cjs.entry.js +1 -1
  18. package/dist/cjs/limel-flatpickr-adapter.cjs.entry.js +1 -1
  19. package/dist/cjs/limel-helper-line_3.cjs.entry.js +454 -0
  20. package/dist/cjs/limel-list-item.cjs.entry.js +1 -1
  21. package/dist/cjs/limel-pagination-jump.cjs.entry.js +182 -0
  22. package/dist/cjs/limel-pagination.cjs.entry.js +165 -126
  23. package/dist/cjs/limel-picker.cjs.entry.js +1 -1
  24. package/dist/cjs/limel-profile-picture.cjs.entry.js +1 -1
  25. package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +1 -1
  26. package/dist/cjs/limel-slider.cjs.entry.js +1 -1
  27. package/dist/cjs/limel-snackbar.cjs.entry.js +1 -1
  28. package/dist/cjs/limel-table.cjs.entry.js +1 -1
  29. package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +1 -1
  30. package/dist/cjs/loader.cjs.js +1 -1
  31. package/dist/cjs/pagination.util-B5zzH8vg.js +135 -0
  32. package/dist/cjs/{translations-C5T2-_rH.js → translations-DFFpR6AZ.js} +32 -0
  33. package/dist/collection/collection-manifest.json +1 -0
  34. package/dist/collection/components/pagination/jump/pagination-jump.css +11 -0
  35. package/dist/collection/components/pagination/jump/pagination-jump.js +331 -0
  36. package/dist/collection/components/pagination/pagination.css +8 -4
  37. package/dist/collection/components/pagination/pagination.js +167 -10
  38. package/dist/collection/components/pagination/pagination.util.js +25 -8
  39. package/dist/collection/translations/da.js +4 -0
  40. package/dist/collection/translations/de.js +4 -0
  41. package/dist/collection/translations/en.js +4 -0
  42. package/dist/collection/translations/fi.js +4 -0
  43. package/dist/collection/translations/fr.js +4 -0
  44. package/dist/collection/translations/nl.js +4 -0
  45. package/dist/collection/translations/no.js +4 -0
  46. package/dist/collection/translations/sv.js +4 -0
  47. package/dist/esm/component-B2Vy44Z3.js +1570 -0
  48. package/dist/esm/lime-elements.js +1 -1
  49. package/dist/esm/limel-action-bar_3.entry.js +1 -1
  50. package/dist/esm/limel-ai-avatar.entry.js +1 -1
  51. package/dist/esm/{limel-breadcrumbs_8.entry.js → limel-breadcrumbs_6.entry.js} +3 -1977
  52. package/dist/esm/limel-callout.entry.js +1 -1
  53. package/dist/esm/limel-chart.entry.js +1 -1
  54. package/dist/esm/limel-chip-set.entry.js +1 -1
  55. package/dist/esm/limel-chip_2.entry.js +1 -1
  56. package/dist/esm/limel-code-diff.entry.js +1 -1
  57. package/dist/esm/limel-code-editor.entry.js +1 -1
  58. package/dist/esm/limel-collapsible-section.entry.js +1 -1
  59. package/dist/esm/limel-drag-handle.entry.js +1 -1
  60. package/dist/esm/limel-email-viewer.entry.js +1 -1
  61. package/dist/esm/limel-file-viewer.entry.js +1 -1
  62. package/dist/esm/limel-file.entry.js +1 -1
  63. package/dist/esm/limel-flatpickr-adapter.entry.js +1 -1
  64. package/dist/esm/limel-helper-line_3.entry.js +450 -0
  65. package/dist/esm/limel-list-item.entry.js +1 -1
  66. package/dist/esm/limel-pagination-jump.entry.js +180 -0
  67. package/dist/esm/limel-pagination.entry.js +165 -126
  68. package/dist/esm/limel-picker.entry.js +1 -1
  69. package/dist/esm/limel-profile-picture.entry.js +1 -1
  70. package/dist/esm/limel-prosemirror-adapter.entry.js +1 -1
  71. package/dist/esm/limel-slider.entry.js +1 -1
  72. package/dist/esm/limel-snackbar.entry.js +1 -1
  73. package/dist/esm/limel-table.entry.js +1 -1
  74. package/dist/esm/limel-text-editor-link-menu.entry.js +1 -1
  75. package/dist/esm/loader.js +1 -1
  76. package/dist/esm/pagination.util-CSC2LJ7P.js +132 -0
  77. package/dist/esm/{translations-Bwm2y2E4.js → translations-DMtX4tTX.js} +32 -0
  78. package/dist/lime-elements/lime-elements.esm.js +1 -1
  79. package/dist/lime-elements/{p-2bbdac10.entry.js → p-11a96a8c.entry.js} +1 -1
  80. package/dist/lime-elements/p-1e93473a.entry.js +1 -0
  81. package/dist/lime-elements/{p-a41b5ff3.entry.js → p-2e568c56.entry.js} +1 -1
  82. package/dist/lime-elements/{p-de631fc3.entry.js → p-2f57c3fa.entry.js} +1 -1
  83. package/dist/lime-elements/p-3735111e.entry.js +1 -0
  84. package/dist/lime-elements/{p-c823054d.entry.js → p-452b9b01.entry.js} +1 -1
  85. package/dist/lime-elements/{p-f333f12d.entry.js → p-4855dc9b.entry.js} +1 -1
  86. package/dist/lime-elements/{p-a462aeb7.entry.js → p-4902fb3f.entry.js} +1 -1
  87. package/dist/lime-elements/p-507cc8a1.entry.js +159 -0
  88. package/dist/lime-elements/{p-dadd2e17.entry.js → p-531b6a64.entry.js} +1 -1
  89. package/dist/lime-elements/{p-b194a212.entry.js → p-5698e493.entry.js} +1 -1
  90. package/dist/lime-elements/{p-65a9adfc.entry.js → p-67f02b69.entry.js} +1 -1
  91. package/dist/lime-elements/{p-1162dd39.entry.js → p-867681aa.entry.js} +1 -1
  92. package/dist/lime-elements/{p-f72ee01b.entry.js → p-90df3e1b.entry.js} +1 -1
  93. package/dist/lime-elements/{p-22f85b99.entry.js → p-955292eb.entry.js} +1 -1
  94. package/dist/lime-elements/{p-c2ff132c.entry.js → p-9b631363.entry.js} +1 -1
  95. package/dist/lime-elements/{p-8af3e4a1.entry.js → p-9d3437ac.entry.js} +1 -1
  96. package/dist/lime-elements/p-B8hoCfhT.js +1 -0
  97. package/dist/lime-elements/p-BwJGg-WI.js +89 -0
  98. package/dist/lime-elements/p-CSC2LJ7P.js +1 -0
  99. package/dist/lime-elements/p-a1de9bbe.entry.js +1 -0
  100. package/dist/lime-elements/{p-b5484494.entry.js → p-acba5700.entry.js} +1 -1
  101. package/dist/lime-elements/{p-f6361b03.entry.js → p-b8c76487.entry.js} +1 -1
  102. package/dist/lime-elements/{p-ac9c5c88.entry.js → p-cafec335.entry.js} +1 -1
  103. package/dist/lime-elements/{p-48928b4f.entry.js → p-d146feda.entry.js} +1 -1
  104. package/dist/lime-elements/{p-532ea234.entry.js → p-deceda00.entry.js} +1 -1
  105. package/dist/lime-elements/{p-e508771f.entry.js → p-f04f7c8e.entry.js} +1 -1
  106. package/dist/lime-elements/{p-51dc5f0b.entry.js → p-f3c108c9.entry.js} +1 -1
  107. package/dist/lime-elements/{p-24041bbb.entry.js → p-fbcd99a3.entry.js} +1 -1
  108. package/dist/types/components/pagination/jump/pagination-jump.d.ts +102 -0
  109. package/dist/types/components/pagination/pagination.d.ts +81 -0
  110. package/dist/types/components/pagination/pagination.util.d.ts +12 -2
  111. package/dist/types/components.d.ts +145 -0
  112. package/dist/types/translations/da.d.ts +4 -0
  113. package/dist/types/translations/de.d.ts +4 -0
  114. package/dist/types/translations/en.d.ts +4 -0
  115. package/dist/types/translations/fi.d.ts +4 -0
  116. package/dist/types/translations/fr.d.ts +4 -0
  117. package/dist/types/translations/nl.d.ts +4 -0
  118. package/dist/types/translations/no.d.ts +4 -0
  119. package/dist/types/translations/sv.d.ts +4 -0
  120. package/package.json +1 -1
  121. package/dist/cjs/limel-helper-line.cjs.entry.js +0 -42
  122. package/dist/esm/limel-helper-line.entry.js +0 -40
  123. package/dist/lime-elements/p-497b7be1.entry.js +0 -248
  124. package/dist/lime-elements/p-DAFuhe5e.js +0 -1
  125. package/dist/lime-elements/p-b4988a9b.entry.js +0 -1
  126. package/dist/lime-elements/p-f81dc23a.entry.js +0 -1
@@ -0,0 +1,331 @@
1
+ import { h, Host, } from "@stencil/core";
2
+ import translate from "../../../global/translations";
3
+ import { ENTER } from "../../../util/keycodes";
4
+ import { pageFromInput } from "../pagination.util";
5
+ const FIRST_PAGE = 1;
6
+ const FALLBACK_LANGUAGE = 'en';
7
+ /**
8
+ * A field for going straight to a page, which `limel-pagination` puts inside
9
+ * the popover its `···` opens.
10
+ *
11
+ * It draws nothing until it is opened, so a pagination that is merely on
12
+ * screen pays for an empty element rather than a form. It lives in a
13
+ * component of its own because the popover carries its content into a shadow
14
+ * root elsewhere on the page, where a parent's stylesheet cannot reach it —
15
+ * a component brings its own.
16
+ *
17
+ * @private
18
+ * @exampleComponent limel-example-pagination-jump-basic
19
+ */
20
+ export class PaginationJump {
21
+ constructor() {
22
+ /**
23
+ * Whether the field is being shown.
24
+ */
25
+ this.open = false;
26
+ /**
27
+ * The page to start from, which is the one the user is on.
28
+ */
29
+ this.page = FIRST_PAGE;
30
+ /**
31
+ * How many pages there are, which is as far as a jump can go.
32
+ */
33
+ this.pageCount = FIRST_PAGE;
34
+ /**
35
+ * Set while a page is being fetched, so that another cannot be asked for.
36
+ */
37
+ this.loading = false;
38
+ /**
39
+ * The language used for the labels and for the way numbers are written.
40
+ */
41
+ this.language = 'en';
42
+ /** What the field shows: the page it opened with, then what is typed. */
43
+ this.value = '';
44
+ this.shouldFocusField = false;
45
+ /**
46
+ * Only to keep what is on screen honest. `limel-input-field` draws an
47
+ * unfocused number through its `value` prop, so a field left pinned to
48
+ * the page it opened with would show that again the moment the keyboard
49
+ * moved to the button, while the button submitted something else.
50
+ *
51
+ * @param event - the field reporting what it now holds
52
+ */
53
+ this.handleChange = (event) => {
54
+ var _a;
55
+ event.stopPropagation();
56
+ this.value = String((_a = event.detail) !== null && _a !== void 0 ? _a : '');
57
+ };
58
+ this.handleKeyDown = (event) => {
59
+ if (event.key !== ENTER) {
60
+ return;
61
+ }
62
+ event.preventDefault();
63
+ this.commit();
64
+ };
65
+ this.commit = () => {
66
+ const page = this.target;
67
+ if (page === null || this.loading) {
68
+ return;
69
+ }
70
+ this.jump.emit(page);
71
+ };
72
+ this.setField = (element) => {
73
+ this.field = element;
74
+ };
75
+ this.translate = (key, params) => {
76
+ return translate.get(key, this.language, params);
77
+ };
78
+ }
79
+ componentWillLoad() {
80
+ this.handleOpenChange();
81
+ }
82
+ handleLanguageChange() {
83
+ this.numberFormat = undefined;
84
+ }
85
+ /**
86
+ * Opening starts the field again rather than leaving what was last typed,
87
+ * so it always offers the page the user is on now.
88
+ */
89
+ handleOpenChange() {
90
+ if (!this.open) {
91
+ return;
92
+ }
93
+ this.value = String(this.page);
94
+ this.shouldFocusField = true;
95
+ }
96
+ componentDidRender() {
97
+ if (!this.shouldFocusField) {
98
+ return;
99
+ }
100
+ this.shouldFocusField = false;
101
+ this.focusField();
102
+ }
103
+ render() {
104
+ if (!this.open) {
105
+ return;
106
+ }
107
+ return (h(Host, { onKeyDown: this.handleKeyDown }, h("limel-input-field", { type: "number", label: this.translate('pagination.page-number'), placeholder: this.pageRange, value: this.value, onChange: this.handleChange, min: FIRST_PAGE, max: this.pageCount, step: 1, ref: this.setField }), h("limel-button", { primary: true, label: this.translate('pagination.go'), disabled: this.loading, onClick: this.commit })));
108
+ }
109
+ /**
110
+ * The page that is in the field now, or `null` when it holds none.
111
+ *
112
+ * Read from the field itself rather than from its `change` event, which
113
+ * arrives on a debounce: a page typed and submitted inside that window
114
+ * would otherwise be submitted as whatever preceded it.
115
+ */
116
+ get target() {
117
+ var _a, _b;
118
+ const typed = (_b = (_a = this.nativeField) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : this.value;
119
+ return pageFromInput(typed, this.pageCount);
120
+ }
121
+ /** `1–492`, so that being moved to 492 is a stated bound, not a surprise. */
122
+ get pageRange() {
123
+ return this.translate('pagination.page-range', {
124
+ first: this.formatNumber(FIRST_PAGE),
125
+ last: this.formatNumber(this.pageCount),
126
+ });
127
+ }
128
+ /** `limel-input-field` wraps a native field but does not expose it. */
129
+ get nativeField() {
130
+ var _a, _b;
131
+ return (_b = (_a = this.field) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('input');
132
+ }
133
+ /**
134
+ * The field delegates focus to the input it wraps, so focusing the host
135
+ * is enough. Selecting as well, so the page the user is on is both the
136
+ * starting value and out of the way of the first keystroke.
137
+ */
138
+ focusField() {
139
+ const host = this.field;
140
+ host === null || host === void 0 ? void 0 : host.componentOnReady().then(() => {
141
+ // A frame later, so the popover has been put where it belongs
142
+ // before the field takes the keyboard.
143
+ requestAnimationFrame(() => {
144
+ var _a, _b;
145
+ host.focus();
146
+ (_b = (_a = host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('input')) === null || _b === void 0 ? void 0 : _b.select();
147
+ });
148
+ });
149
+ }
150
+ /**
151
+ * The parent warns about a language it cannot use, so this one only has
152
+ * to keep drawing.
153
+ *
154
+ * @param value - the number to write out
155
+ * @returns the number in the current language
156
+ */
157
+ formatNumber(value) {
158
+ var _a;
159
+ (_a = this.numberFormat) !== null && _a !== void 0 ? _a : (this.numberFormat = this.createNumberFormat());
160
+ return this.numberFormat.format(value);
161
+ }
162
+ /**
163
+ * The pagination hands down the language it settled on, so this only has
164
+ * to survive being handed one directly, as the example page does.
165
+ *
166
+ * @returns a formatter, in that language where it can be used
167
+ */
168
+ createNumberFormat() {
169
+ try {
170
+ return new Intl.NumberFormat(this.language);
171
+ }
172
+ catch (_a) {
173
+ return new Intl.NumberFormat(FALLBACK_LANGUAGE);
174
+ }
175
+ }
176
+ static get is() { return "limel-pagination-jump"; }
177
+ static get encapsulation() { return "shadow"; }
178
+ static get originalStyleUrls() {
179
+ return {
180
+ "$": ["pagination-jump.scss"]
181
+ };
182
+ }
183
+ static get styleUrls() {
184
+ return {
185
+ "$": ["pagination-jump.css"]
186
+ };
187
+ }
188
+ static get properties() {
189
+ return {
190
+ "open": {
191
+ "type": "boolean",
192
+ "mutable": false,
193
+ "complexType": {
194
+ "original": "boolean",
195
+ "resolved": "boolean",
196
+ "references": {}
197
+ },
198
+ "required": false,
199
+ "optional": false,
200
+ "docs": {
201
+ "tags": [],
202
+ "text": "Whether the field is being shown."
203
+ },
204
+ "getter": false,
205
+ "setter": false,
206
+ "reflect": true,
207
+ "attribute": "open",
208
+ "defaultValue": "false"
209
+ },
210
+ "page": {
211
+ "type": "number",
212
+ "mutable": false,
213
+ "complexType": {
214
+ "original": "number",
215
+ "resolved": "number",
216
+ "references": {}
217
+ },
218
+ "required": false,
219
+ "optional": false,
220
+ "docs": {
221
+ "tags": [],
222
+ "text": "The page to start from, which is the one the user is on."
223
+ },
224
+ "getter": false,
225
+ "setter": false,
226
+ "reflect": false,
227
+ "attribute": "page",
228
+ "defaultValue": "1"
229
+ },
230
+ "pageCount": {
231
+ "type": "number",
232
+ "mutable": false,
233
+ "complexType": {
234
+ "original": "number",
235
+ "resolved": "number",
236
+ "references": {}
237
+ },
238
+ "required": false,
239
+ "optional": false,
240
+ "docs": {
241
+ "tags": [],
242
+ "text": "How many pages there are, which is as far as a jump can go."
243
+ },
244
+ "getter": false,
245
+ "setter": false,
246
+ "reflect": false,
247
+ "attribute": "page-count",
248
+ "defaultValue": "1"
249
+ },
250
+ "loading": {
251
+ "type": "boolean",
252
+ "mutable": false,
253
+ "complexType": {
254
+ "original": "boolean",
255
+ "resolved": "boolean",
256
+ "references": {}
257
+ },
258
+ "required": false,
259
+ "optional": false,
260
+ "docs": {
261
+ "tags": [],
262
+ "text": "Set while a page is being fetched, so that another cannot be asked for."
263
+ },
264
+ "getter": false,
265
+ "setter": false,
266
+ "reflect": false,
267
+ "attribute": "loading",
268
+ "defaultValue": "false"
269
+ },
270
+ "language": {
271
+ "type": "string",
272
+ "mutable": false,
273
+ "complexType": {
274
+ "original": "Languages",
275
+ "resolved": "\"da\" | \"de\" | \"en\" | \"fi\" | \"fr\" | \"nb\" | \"nl\" | \"no\" | \"sv\"",
276
+ "references": {
277
+ "Languages": {
278
+ "location": "import",
279
+ "path": "../../date-picker/date.types",
280
+ "id": "src/components/date-picker/date.types.ts::Languages",
281
+ "referenceLocation": "Languages"
282
+ }
283
+ }
284
+ },
285
+ "required": false,
286
+ "optional": false,
287
+ "docs": {
288
+ "tags": [],
289
+ "text": "The language used for the labels and for the way numbers are written."
290
+ },
291
+ "getter": false,
292
+ "setter": false,
293
+ "reflect": false,
294
+ "attribute": "language",
295
+ "defaultValue": "'en'"
296
+ }
297
+ };
298
+ }
299
+ static get states() {
300
+ return {
301
+ "value": {}
302
+ };
303
+ }
304
+ static get events() {
305
+ return [{
306
+ "method": "jump",
307
+ "name": "jump",
308
+ "bubbles": true,
309
+ "cancelable": true,
310
+ "composed": true,
311
+ "docs": {
312
+ "tags": [],
313
+ "text": "Asks for the page that was typed, already brought within the set."
314
+ },
315
+ "complexType": {
316
+ "original": "number",
317
+ "resolved": "number",
318
+ "references": {}
319
+ }
320
+ }];
321
+ }
322
+ static get watchers() {
323
+ return [{
324
+ "propName": "language",
325
+ "methodName": "handleLanguageChange"
326
+ }, {
327
+ "propName": "open",
328
+ "methodName": "handleOpenChange"
329
+ }];
330
+ }
331
+ }
@@ -186,14 +186,18 @@ button limel-spinner {
186
186
  pointer-events: none;
187
187
  }
188
188
 
189
- .gap {
189
+ limel-popover {
190
190
  display: inline-flex;
191
- align-items: center;
192
- justify-content: center;
193
- height: 1.75rem;
191
+ }
192
+
193
+ .gap {
194
194
  width: 1.75rem;
195
+ padding: 0;
195
196
  color: rgb(var(--contrast-1100));
196
197
  }
198
+ .gap[aria-expanded=true] {
199
+ box-shadow: var(--button-shadow-inset-pressed) !important;
200
+ }
197
201
 
198
202
  .arrow span {
199
203
  display: block;
@@ -15,6 +15,10 @@ const FALLBACK_LANGUAGE = 'en';
15
15
  * looking at, and how far into it they have got; and hovering a page shows
16
16
  * exactly which items it holds.
17
17
  *
18
+ * Where there are more pages than fit, the ones left out are stood for by a
19
+ * `···`. That is a button: it opens a field for going straight to any page in
20
+ * the set, so no page is more than one move away however long the set is.
21
+ *
18
22
  * @exampleComponent limel-example-pagination-basic
19
23
  * @exampleComponent limel-example-pagination-single-page
20
24
  * @exampleComponent limel-example-pagination-page-size
@@ -63,12 +67,15 @@ export class Pagination {
63
67
  this.idPrefix = createRandomString();
64
68
  /** The value each prop was last complained about with. */
65
69
  this.warnedAbout = new Map();
70
+ /** The language `numberFormat` settled on, which may not be the prop. */
71
+ this.settledLanguage = FALLBACK_LANGUAGE;
66
72
  /** Settled once per render, and the only thing `render` reads. */
67
73
  this.view = {
68
74
  page: FIRST_PAGE,
69
75
  pageCount: null,
70
76
  pageSize: DEFAULT_PAGE_SIZE,
71
77
  totalItems: null,
78
+ slots: [],
72
79
  };
73
80
  /**
74
81
  * A gap marker, or a page button.
@@ -79,11 +86,38 @@ export class Pagination {
79
86
  */
80
87
  this.renderSlot = (slot, index) => {
81
88
  if (slot.kind === 'gap') {
82
- return (h("span", { key: `gap-${index}`, class: "gap", "aria-hidden": "true" }, "\u00B7\u00B7\u00B7"));
89
+ return this.renderGap(index);
83
90
  }
84
91
  const isCurrent = slot.page === this.currentPage;
85
92
  return (h("button", { key: `page-${index}`, id: this.slotId(index), class: "page", "aria-current": isCurrent ? 'page' : null, "aria-disabled": this.loading ? 'true' : null, "aria-label": this.pageLabel(slot.page, isCurrent), "data-page": slot.page, onClick: this.selectPage }, this.formatNumber(slot.page), this.renderSpinner(isCurrent), this.renderTooltip(slot.page, index)));
86
93
  };
94
+ this.openJump = (event) => {
95
+ if (this.loading) {
96
+ return;
97
+ }
98
+ const { index } = event.currentTarget.dataset;
99
+ this.openGap = Number(index);
100
+ };
101
+ this.closeJump = () => {
102
+ this.openGap = undefined;
103
+ };
104
+ /**
105
+ * The control is told what to show, so a jump only asks. Focus goes back
106
+ * to the marker the field came out of, which is still on screen and will
107
+ * still be somewhere sensible if the consumer declines.
108
+ *
109
+ * @param event - the page the field asks for
110
+ */
111
+ this.handleJump = (event) => {
112
+ event.stopPropagation();
113
+ const page = event.detail;
114
+ this.focusGap(this.openGap);
115
+ this.openGap = undefined;
116
+ if (this.goTo(page)) {
117
+ this.awaitedJump = page;
118
+ this.jumpedFrom = this.view.page;
119
+ }
120
+ };
87
121
  // Bound once: a handler built per render is a new identity each time, so
88
122
  // every button would have its listener swapped on every page change.
89
123
  this.step = (event) => {
@@ -95,9 +129,6 @@ export class Pagination {
95
129
  this.goTo(this.currentPage + (direction === 'next' ? 1 : -1));
96
130
  };
97
131
  this.selectPage = (event) => {
98
- if (this.loading) {
99
- return;
100
- }
101
132
  const { page } = event.currentTarget.dataset;
102
133
  this.goTo(Number(page));
103
134
  };
@@ -127,7 +158,9 @@ export class Pagination {
127
158
  pageCount: pageCount,
128
159
  pageSize: pageSize,
129
160
  totalItems: totalItems,
161
+ slots: this.slotsFor(page, pageCount),
130
162
  };
163
+ this.closeJumpWithoutAGap();
131
164
  this.readOut(page);
132
165
  this.reconcile(page, usable);
133
166
  this.shownPage = page;
@@ -144,6 +177,10 @@ export class Pagination {
144
177
  return Math.max(FIRST_PAGE, Math.ceil(total / pageSize));
145
178
  }
146
179
  componentDidRender() {
180
+ this.reportCorrection();
181
+ this.followJumpedPage();
182
+ }
183
+ reportCorrection() {
147
184
  const page = this.pendingCorrection;
148
185
  if (page === undefined) {
149
186
  return;
@@ -250,10 +287,23 @@ export class Pagination {
250
287
  handleLanguageChange() {
251
288
  this.numberFormat = undefined;
252
289
  }
290
+ /**
291
+ * A gap is remembered by the position it sits in, and the positions are
292
+ * worked out afresh every render. A page set from outside can put a page
293
+ * number where the open gap was, taking the popover down without it ever
294
+ * closing — and leaving it to spring open by itself the next time a gap
295
+ * lands there.
296
+ */
297
+ closeJumpWithoutAGap() {
298
+ var _a;
299
+ const open = this.openGap;
300
+ if (open !== undefined && ((_a = this.view.slots[open]) === null || _a === void 0 ? void 0 : _a.kind) !== 'gap') {
301
+ this.openGap = undefined;
302
+ }
303
+ }
253
304
  render() {
254
- const { page, pageCount } = this.view;
255
- const slots = this.slotsFor(page, pageCount);
256
- return (h(Host, { key: '40d6123ac887f519fc31690fce7fcb46e73f2199' }, h("nav", { key: '183d264e761d168c74b8e2bfa8d04f3ee391c937', "aria-label": this.translate('pagination.label'), "aria-busy": this.loading ? 'true' : null }, this.renderArrow('previous'), slots.map((slot, index) => this.renderSlot(slot, index)), this.renderArrow('next')), h("span", { key: 'e59f62c2644a9a4a513ab3a3455854084434aabc', class: "live-region", "aria-live": "polite" }, this.liveMessage)));
305
+ const { slots } = this.view;
306
+ return (h(Host, { key: 'fac46260ef2a4e1e8d4aad962c90477831b6497f' }, h("nav", { key: '239f0ba745b9a385064c4a2c91a572e70a9e6525', "aria-label": this.translate('pagination.label'), "aria-busy": this.loading ? 'true' : null }, this.renderArrow('previous'), slots.map((slot, index) => this.renderSlot(slot, index)), this.renderArrow('next')), h("span", { key: '82fa3c9fb3fa8063be64d8373158d3d0e93138c1', class: "live-region", "aria-live": "polite" }, this.liveMessage)));
257
307
  }
258
308
  /**
259
309
  * Without a count we know where the user is but not how far the set goes,
@@ -270,6 +320,92 @@ export class Pagination {
270
320
  }
271
321
  return getPageSlots(page, pageCount);
272
322
  }
323
+ /**
324
+ * A gap is a button, not a marker: the pages it stands for are still
325
+ * reachable, and typing one is the only way to reach any of them in a
326
+ * single move.
327
+ *
328
+ * Both gaps open the same field, and it accepts any page in the set.
329
+ * Scoping each one to the pages it happens to hide would make two
330
+ * identical markers behave differently, with nothing on screen to say
331
+ * which is which.
332
+ *
333
+ * @param index - where the gap sits in the window
334
+ * @returns the rendered gap
335
+ */
336
+ renderGap(index) {
337
+ const { pageCount } = this.view;
338
+ // A gap stands for pages that were left out, which cannot have
339
+ // happened before the count arrived: until then `slotsFor` renders
340
+ // the page the user is on and nothing else.
341
+ if (pageCount === null) {
342
+ return;
343
+ }
344
+ return (h("limel-popover", { key: `gap-${index}`, open: this.openGap === index, openDirection: "bottom", onClose: this.closeJump }, h("button", { slot: "trigger", class: "gap", id: this.slotId(index), "aria-label": this.translate('pagination.jump-to-a-page'), "aria-disabled": this.loading ? 'true' : null, "data-index": index, onClick: this.openJump }, "\u00B7\u00B7\u00B7"), h("limel-pagination-jump", { open: this.openGap === index, page: this.view.page, pageCount: pageCount, loading: this.loading, language: this.languageForNumbers, onJump: this.handleJump })));
345
+ }
346
+ focusGap(index) {
347
+ var _a;
348
+ const gap = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`[id="${this.slotId(index)}"]`);
349
+ gap === null || gap === void 0 ? void 0 : gap.focus();
350
+ this.jumpFocus = gap;
351
+ }
352
+ /**
353
+ * Put the keyboard on the page a jump asked for, once the consumer has
354
+ * shown it.
355
+ *
356
+ * Committing tears down the popover the field was in, so between asking
357
+ * and arriving focus is on nothing. If the user has since put it somewhere
358
+ * of their own, arriving is no longer ours to report.
359
+ */
360
+ followJumpedPage() {
361
+ if (this.awaitedJump === undefined) {
362
+ return;
363
+ }
364
+ if (!this.ownsFocus()) {
365
+ this.forgetJump();
366
+ return;
367
+ }
368
+ if (this.view.page !== this.awaitedJump) {
369
+ // Still showing the page the jump was asked from is what waiting
370
+ // looks like. Anywhere else is an answer too — the consumer chose
371
+ // to show something else — and a later arrival at the page we
372
+ // asked for is then no longer ours to follow.
373
+ if (this.view.page !== this.jumpedFrom) {
374
+ this.forgetJump();
375
+ }
376
+ return;
377
+ }
378
+ this.forgetJump();
379
+ this.focusCurrentPage();
380
+ }
381
+ forgetJump() {
382
+ this.awaitedJump = undefined;
383
+ this.jumpedFrom = undefined;
384
+ }
385
+ /**
386
+ * Whether the keyboard is still where the jump put it. Somewhere else
387
+ * inside this component means the user moved it and it is theirs.
388
+ *
389
+ * The marker itself can also go: jumping from the left gap to page 3
390
+ * turns that slot into a page number, and the button holding focus is
391
+ * taken away with it. A browser answers that by focusing the body rather
392
+ * than nothing, so the marker having left the page is what we look for.
393
+ */
394
+ ownsFocus() {
395
+ var _a, _b;
396
+ const inside = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement;
397
+ if (inside === this.jumpFocus) {
398
+ return true;
399
+ }
400
+ return (!((_b = this.jumpFocus) === null || _b === void 0 ? void 0 : _b.isConnected) &&
401
+ document.activeElement === document.body);
402
+ }
403
+ focusCurrentPage() {
404
+ var _a;
405
+ const button = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('[aria-current="page"]');
406
+ button === null || button === void 0 ? void 0 : button.focus();
407
+ this.jumpFocus = undefined;
408
+ }
273
409
  /**
274
410
  * A spinner around the page the user is heading for, while it loads.
275
411
  *
@@ -332,13 +468,19 @@ export class Pagination {
332
468
  return pageCount === null || page >= pageCount;
333
469
  }
334
470
  /**
471
+ * The one path every move the user asks for goes through, and so the one
472
+ * place that refuses while a page is still being fetched.
473
+ *
335
474
  * @param page - the page to ask for, 1-based
475
+ * @returns whether it was asked for
336
476
  */
337
477
  goTo(page) {
338
- if (page === this.currentPage) {
339
- return;
478
+ this.awaitedJump = undefined;
479
+ if (this.loading || page === this.currentPage) {
480
+ return false;
340
481
  }
341
482
  this.emitGoToPage(page, 'user');
483
+ return true;
342
484
  }
343
485
  /** The page on screen: the one asked for, within the range that exists. */
344
486
  get currentPage() {
@@ -474,6 +616,17 @@ export class Pagination {
474
616
  (_a = this.numberFormat) !== null && _a !== void 0 ? _a : (this.numberFormat = this.createNumberFormat());
475
617
  return this.numberFormat.format(value);
476
618
  }
619
+ /**
620
+ * What the jump form is given, so that the one warning about a language
621
+ * this browser cannot write numbers in describes both of us. A tag it
622
+ * accepts but does not support, `zz`, resolves to the viewer's own locale
623
+ * rather than throwing, so asking the formatter is the only way to know.
624
+ */
625
+ get languageForNumbers() {
626
+ var _a;
627
+ (_a = this.numberFormat) !== null && _a !== void 0 ? _a : (this.numberFormat = this.createNumberFormat());
628
+ return this.settledLanguage;
629
+ }
477
630
  /**
478
631
  * `Intl.NumberFormat` throws on a language it cannot parse, and this is
479
632
  * called from `render`, so an unusable one would leave the control drawing
@@ -494,6 +647,7 @@ export class Pagination {
494
647
  const resolved = format.resolvedOptions().locale;
495
648
  if (getPrimarySubtag(resolved) === getPrimarySubtag(language)) {
496
649
  this.warnedAbout.delete('language');
650
+ this.settledLanguage = language;
497
651
  return format;
498
652
  }
499
653
  }
@@ -504,6 +658,7 @@ export class Pagination {
504
658
  // Where the translations land for the same unusable value, rather than
505
659
  // the viewer's own locale — English labels beside system digit
506
660
  // grouping would read differently on every machine.
661
+ this.settledLanguage = FALLBACK_LANGUAGE;
507
662
  return new Intl.NumberFormat(FALLBACK_LANGUAGE);
508
663
  }
509
664
  static get is() { return "limel-pagination"; }
@@ -631,7 +786,8 @@ export class Pagination {
631
786
  }
632
787
  static get states() {
633
788
  return {
634
- "liveMessage": {}
789
+ "liveMessage": {},
790
+ "openGap": {}
635
791
  };
636
792
  }
637
793
  static get events() {
@@ -659,6 +815,7 @@ export class Pagination {
659
815
  }
660
816
  }];
661
817
  }
818
+ static get elementRef() { return "host"; }
662
819
  static get watchers() {
663
820
  return [{
664
821
  "propName": "language",
@@ -8,6 +8,30 @@ export const VISIBLE_SLOTS = 7;
8
8
  /** How many pages to keep either side of the current one. */
9
9
  const SIBLINGS = 1;
10
10
  const FIRST_PAGE = 1;
11
+ /**
12
+ * The page a typed value asks for, or `null` when it asks for nothing.
13
+ *
14
+ * Out of range is not an error. The set has a first and a last page, both
15
+ * written under the field, and the nearer of them is what someone typing past
16
+ * the end meant.
17
+ *
18
+ * @param value - what was typed into the field
19
+ * @param pageCount - how many pages there are, or `null` when not yet known
20
+ * @returns a page within the set, or `null` when there is no number to use
21
+ */
22
+ export function pageFromInput(value, pageCount) {
23
+ const typed = Number.parseInt(value, 10);
24
+ if (!Number.isSafeInteger(typed)) {
25
+ return null;
26
+ }
27
+ const atLeastFirst = Math.max(typed, FIRST_PAGE);
28
+ // Each bound on its own. Folding them into one `Math.min` against
29
+ // `pageCount ?? typed` handed the typed value back as its own upper
30
+ // bound, so without a count `0` stayed `0` rather than becoming page 1.
31
+ return pageCount === null
32
+ ? atLeastFirst
33
+ : Math.min(atLeastFirst, pageCount);
34
+ }
11
35
  /**
12
36
  * Build the list of positions to render for a given page count.
13
37
  *
@@ -101,13 +125,6 @@ function withGaps(pages) {
101
125
  if (skipped === 1) {
102
126
  return [{ kind: 'page', page: page - 1 }, slot];
103
127
  }
104
- return [
105
- {
106
- kind: 'gap',
107
- from: pages[index - 1] + 1,
108
- to: page - 1,
109
- },
110
- slot,
111
- ];
128
+ return [{ kind: 'gap' }, slot];
112
129
  });
113
130
  }