@limetech/lime-elements 40.21.0 → 40.22.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 (124) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/language-s0bKUCvO.js +58 -0
  3. package/dist/cjs/lime-elements.cjs.js +1 -1
  4. package/dist/cjs/limel-action-bar_3.cjs.entry.js +2 -2
  5. package/dist/cjs/limel-ai-avatar.cjs.entry.js +2 -2
  6. package/dist/cjs/limel-badge_7.cjs.entry.js +12 -2
  7. package/dist/cjs/limel-callout.cjs.entry.js +2 -2
  8. package/dist/cjs/limel-chart.cjs.entry.js +524 -76
  9. package/dist/cjs/limel-chip-set.cjs.entry.js +2 -2
  10. package/dist/cjs/limel-chip_2.cjs.entry.js +2 -2
  11. package/dist/cjs/limel-code-diff.cjs.entry.js +2 -2
  12. package/dist/cjs/limel-code-editor.cjs.entry.js +2 -2
  13. package/dist/cjs/limel-collapsible-section.cjs.entry.js +2 -2
  14. package/dist/cjs/limel-date-picker.cjs.entry.js +1 -1
  15. package/dist/cjs/limel-dock.cjs.entry.js +2 -2
  16. package/dist/cjs/limel-drag-handle_2.cjs.entry.js +2 -2
  17. package/dist/cjs/limel-email-viewer.cjs.entry.js +2 -2
  18. package/dist/cjs/limel-file-viewer.cjs.entry.js +164 -2
  19. package/dist/cjs/limel-file.cjs.entry.js +2 -2
  20. package/dist/cjs/limel-flatpickr-adapter.cjs.entry.js +2 -2
  21. package/dist/cjs/limel-list-item.cjs.entry.js +2 -2
  22. package/dist/cjs/limel-pagination-jump.cjs.entry.js +2 -2
  23. package/dist/cjs/limel-pagination.cjs.entry.js +14 -26
  24. package/dist/cjs/limel-picker.cjs.entry.js +70 -27
  25. package/dist/cjs/limel-profile-picture.cjs.entry.js +2 -2
  26. package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +2 -2
  27. package/dist/cjs/limel-resize-handle.cjs.entry.js +2 -2
  28. package/dist/cjs/limel-side-panel.cjs.entry.js +2 -2
  29. package/dist/cjs/limel-slider.cjs.entry.js +2 -2
  30. package/dist/cjs/limel-snackbar.cjs.entry.js +2 -2
  31. package/dist/cjs/limel-table.cjs.entry.js +2 -2
  32. package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +2 -2
  33. package/dist/cjs/loader.cjs.js +1 -1
  34. package/dist/cjs/{translations-CqJEWcfw.js → translations-JLP5icQ9.js} +1 -1
  35. package/dist/collection/components/chart/chart.js +116 -45
  36. package/dist/collection/components/chart/format.js +302 -0
  37. package/dist/collection/components/chart/options.js +26 -14
  38. package/dist/collection/components/chart/scale.js +132 -19
  39. package/dist/collection/components/input-field/input-field.js +10 -0
  40. package/dist/collection/components/pagination/pagination.js +13 -25
  41. package/dist/collection/components/picker/picker.js +67 -24
  42. package/dist/collection/util/language.js +29 -0
  43. package/dist/esm/language-VamkUr90.js +55 -0
  44. package/dist/esm/lime-elements.js +1 -1
  45. package/dist/esm/limel-action-bar_3.entry.js +2 -2
  46. package/dist/esm/limel-ai-avatar.entry.js +2 -2
  47. package/dist/esm/limel-badge_7.entry.js +12 -2
  48. package/dist/esm/limel-callout.entry.js +2 -2
  49. package/dist/esm/limel-chart.entry.js +524 -76
  50. package/dist/esm/limel-chip-set.entry.js +2 -2
  51. package/dist/esm/limel-chip_2.entry.js +2 -2
  52. package/dist/esm/limel-code-diff.entry.js +2 -2
  53. package/dist/esm/limel-code-editor.entry.js +2 -2
  54. package/dist/esm/limel-collapsible-section.entry.js +2 -2
  55. package/dist/esm/limel-date-picker.entry.js +1 -1
  56. package/dist/esm/limel-dock.entry.js +2 -2
  57. package/dist/esm/limel-drag-handle_2.entry.js +2 -2
  58. package/dist/esm/limel-email-viewer.entry.js +2 -2
  59. package/dist/esm/limel-file-viewer.entry.js +164 -2
  60. package/dist/esm/limel-file.entry.js +2 -2
  61. package/dist/esm/limel-flatpickr-adapter.entry.js +2 -2
  62. package/dist/esm/limel-list-item.entry.js +2 -2
  63. package/dist/esm/limel-pagination-jump.entry.js +2 -2
  64. package/dist/esm/limel-pagination.entry.js +14 -26
  65. package/dist/esm/limel-picker.entry.js +71 -28
  66. package/dist/esm/limel-profile-picture.entry.js +2 -2
  67. package/dist/esm/limel-prosemirror-adapter.entry.js +2 -2
  68. package/dist/esm/limel-resize-handle.entry.js +2 -2
  69. package/dist/esm/limel-side-panel.entry.js +2 -2
  70. package/dist/esm/limel-slider.entry.js +2 -2
  71. package/dist/esm/limel-snackbar.entry.js +2 -2
  72. package/dist/esm/limel-table.entry.js +2 -2
  73. package/dist/esm/limel-text-editor-link-menu.entry.js +2 -2
  74. package/dist/esm/loader.js +1 -1
  75. package/dist/esm/{translations-kct5Kybu.js → translations-D9ZGrMhe.js} +1 -1
  76. package/dist/lime-elements/lime-elements.esm.js +1 -1
  77. package/dist/lime-elements/{p-6e9eeb0a.entry.js → p-0a7cbdc3.entry.js} +1 -1
  78. package/dist/lime-elements/{p-d35d2859.entry.js → p-0f96e94d.entry.js} +1 -1
  79. package/dist/lime-elements/{p-a7be03e1.entry.js → p-1a0c5e45.entry.js} +1 -1
  80. package/dist/lime-elements/{p-5e408c27.entry.js → p-401d5c92.entry.js} +1 -1
  81. package/dist/lime-elements/{p-6d2d25b9.entry.js → p-430db3b9.entry.js} +1 -1
  82. package/dist/lime-elements/{p-a6c6a31b.entry.js → p-43b62587.entry.js} +1 -1
  83. package/dist/lime-elements/{p-958396b5.entry.js → p-48dccb83.entry.js} +1 -1
  84. package/dist/lime-elements/{p-814b4934.entry.js → p-5c918155.entry.js} +1 -1
  85. package/dist/lime-elements/{p-22e9e327.entry.js → p-66101ed6.entry.js} +1 -1
  86. package/dist/lime-elements/{p-d8559847.entry.js → p-7ab72b52.entry.js} +1 -1
  87. package/dist/lime-elements/{p-7ab726c2.entry.js → p-8d9a2a22.entry.js} +1 -1
  88. package/dist/lime-elements/{p-55fa19ae.entry.js → p-8de4f0f3.entry.js} +1 -1
  89. package/dist/lime-elements/p-8e80a884.entry.js +56 -0
  90. package/dist/lime-elements/{p-bffb5ed2.entry.js → p-99984c66.entry.js} +1 -1
  91. package/dist/lime-elements/{p-56e2fa48.entry.js → p-9d46f720.entry.js} +1 -1
  92. package/dist/lime-elements/p-9e2b426c.entry.js +1 -0
  93. package/dist/lime-elements/{p-Dzp8dXTo.js → p-C9dBa_Im.js} +1 -1
  94. package/dist/lime-elements/p-VamkUr90.js +1 -0
  95. package/dist/lime-elements/{p-3ac39bd5.entry.js → p-b1c7033b.entry.js} +2 -2
  96. package/dist/lime-elements/{p-47fe76a8.entry.js → p-b5451350.entry.js} +1 -1
  97. package/dist/lime-elements/{p-9edd79a8.entry.js → p-c75f3b0d.entry.js} +1 -1
  98. package/dist/lime-elements/{p-4c080cd9.entry.js → p-ca4247c4.entry.js} +1 -1
  99. package/dist/lime-elements/{p-1a7d1d2b.entry.js → p-d53421af.entry.js} +1 -1
  100. package/dist/lime-elements/{p-6d9b89db.entry.js → p-d8901e39.entry.js} +1 -1
  101. package/dist/lime-elements/{p-933ffaa8.entry.js → p-d89553b3.entry.js} +1 -1
  102. package/dist/lime-elements/{p-496b2437.entry.js → p-d93b8ce0.entry.js} +1 -1
  103. package/dist/lime-elements/{p-23067836.entry.js → p-e97340c4.entry.js} +1 -1
  104. package/dist/lime-elements/{p-7b87e547.entry.js → p-ec0c113d.entry.js} +1 -1
  105. package/dist/lime-elements/p-f197000b.entry.js +159 -0
  106. package/dist/lime-elements/{p-07f3565b.entry.js → p-f6bc6658.entry.js} +1 -1
  107. package/dist/lime-elements/{p-7fcd48c6.entry.js → p-fa1aa440.entry.js} +1 -1
  108. package/dist/types/components/chart/chart.d.ts +32 -7
  109. package/dist/types/components/chart/chart.types.d.ts +15 -1
  110. package/dist/types/components/chart/format.d.ts +54 -0
  111. package/dist/types/components/chart/options.d.ts +3 -4
  112. package/dist/types/components/chart/scale.d.ts +39 -7
  113. package/dist/types/components/input-field/input-field.d.ts +1 -0
  114. package/dist/types/components/pagination/pagination.d.ts +6 -12
  115. package/dist/types/components/picker/picker.d.ts +10 -2
  116. package/dist/types/components.d.ts +36 -8
  117. package/dist/types/util/language.d.ts +19 -0
  118. package/package.json +2 -2
  119. package/dist/cjs/language-v-MMS6ym.js +0 -28
  120. package/dist/esm/language-ChagAaNf.js +0 -26
  121. package/dist/lime-elements/p-7d6a12fe.entry.js +0 -56
  122. package/dist/lime-elements/p-ChagAaNf.js +0 -1
  123. package/dist/lime-elements/p-a807a45f.entry.js +0 -159
  124. package/dist/lime-elements/p-cd479c23.entry.js +0 -1
@@ -1,12 +1,12 @@
1
1
  import { format } from "echarts/core";
2
- import { formatValue, getAxisRange, getMaximumValue, getMinimumValue, getScatterRanges, getXValue, getYValue, getShare, getValueSpan, getWholeRange, isPartOfWhole, isRangeItem, } from "./scale";
2
+ import { getAxisRange, getDateRange, getDateTicks, getMaximumValue, getMinimumValue, getScatterRanges, getXValue, getYValue, getShare, getValueSpan, getWholeRange, isPartOfWhole, isRangeItem, } from "./scale";
3
+ import { formatShare, formatValue, getAxisValueFormatter, } from "./format";
3
4
  const DOT_SIZE = 8;
4
5
  const LINE_DOT_SIZE = 6;
5
6
  const CONNECTOR_WIDTH = 2;
6
7
  const DIMMED_OPACITY = 0.4;
7
8
  const AREA_OPACITY = 0.4;
8
9
  const PADDING = 8;
9
- const PERCENT_DECIMALS = 2;
10
10
  const NPS_MIN = -100;
11
11
  const NPS_MAX = 100;
12
12
  const NPS_START_ANGLE = 200;
@@ -96,8 +96,8 @@ export function getItemLabel(settings, index) {
96
96
  return item.text;
97
97
  }
98
98
  const whole = getWholeRange(settings.items, settings.maxValue);
99
- const share = getShare(item, whole).toFixed(PERCENT_DECIMALS);
100
- return `${item.text} (${share}%)`;
99
+ const share = formatShare(getShare(item, whole), settings.language);
100
+ return `${item.text} (${share})`;
101
101
  }
102
102
  /**
103
103
  * The tooltip of an item, as HTML: its label and its value.
@@ -109,7 +109,7 @@ export function getTooltip(settings, index) {
109
109
  if (!item) {
110
110
  return '';
111
111
  }
112
- return [getItemLabel(settings, index), formatValue(item, settings.type)]
112
+ return [getItemLabel(settings, index), formatValue(item, settings)]
113
113
  .map((text) => format.encodeHTML(text))
114
114
  .join('<br/>');
115
115
  }
@@ -138,12 +138,17 @@ function drawSeries(settings, theme, itemSpace) {
138
138
  }
139
139
  function drawOnAxes(settings, theme, itemSpace) {
140
140
  const { items, orientation } = settings;
141
- const range = getAxisRange(items, settings.maxValue, settings.axisIncrement);
141
+ const range = settings.valueFormat === 'date'
142
+ ? getDateRange(items.flatMap((item) => [
143
+ getMinimumValue(item),
144
+ getMaximumValue(item),
145
+ ]), settings.maxValue)
146
+ : getAxisRange(items, settings.maxValue, settings.axisIncrement);
142
147
  const itemTextAxis = getItemTextAxis(settings);
143
148
  const categoryAxis = Object.assign(Object.assign(Object.assign({}, getCategoryAxis(settings)), { axisLabel: Object.assign({ show: settings.displayItemText, color: theme.textColor }, (itemTextAxis &&
144
149
  itemSpace &&
145
150
  getItemTextLayout(itemTextAxis, itemSpace))), axisTick: hidden, axisLine: { lineStyle: { color: theme.axisLineColor } } }), getAxisName(settings, settings.itemsLabel, theme));
146
- const valueAxis = getValueAxis(range, getAxisName(settings, settings.valuesLabel, theme), theme);
151
+ const valueAxis = getValueAxis(range, getAxisName(settings, settings.valuesLabel, theme), settings, theme);
147
152
  const { series, seriesIndex } = getAxisSeries(settings, theme);
148
153
  const option = {
149
154
  grid: getGrid(theme),
@@ -306,10 +311,15 @@ function getRangeStarts(items) {
306
311
  }
307
312
  function drawScatter(settings, theme) {
308
313
  const { items } = settings;
309
- const ranges = getScatterRanges(items);
314
+ const ranges = settings.valueFormat === 'date'
315
+ ? {
316
+ x: getDateRange(items.map((item) => getXValue(item))),
317
+ y: getDateRange(items.map((item) => getYValue(item))),
318
+ }
319
+ : getScatterRanges(items);
310
320
  const landscape = settings.orientation === 'landscape';
311
- const firstAxis = getValueAxis(ranges.x, getAxisName(settings, settings.itemsLabel, theme), theme);
312
- const secondAxis = getValueAxis(ranges.y, getAxisName(settings, settings.valuesLabel, theme), theme);
321
+ const firstAxis = getValueAxis(ranges.x, getAxisName(settings, settings.itemsLabel, theme), settings, theme);
322
+ const secondAxis = getValueAxis(ranges.y, getAxisName(settings, settings.valuesLabel, theme), settings, theme);
313
323
  const scatter = {
314
324
  type: 'scatter',
315
325
  symbolSize: theme.scatterItemSize,
@@ -531,8 +541,10 @@ function getGrid(theme) {
531
541
  borderWidth: 0,
532
542
  };
533
543
  }
534
- function getValueAxis(range, name, theme) {
535
- return Object.assign({ type: 'value', min: range.minValue, max: range.maxValue, interval: range.increment, axisLabel: { show: true, color: theme.textColor }, axisLine: { lineStyle: { color: theme.axisLineColor } }, splitLine: { lineStyle: { color: theme.axisLineColor } } }, name);
544
+ function getValueAxis(range, name, settings, theme) {
545
+ const dates = settings.valueFormat === 'date' && getDateTicks(range);
546
+ const ticks = dates && { customValues: dates.values };
547
+ return Object.assign(Object.assign(Object.assign({ type: 'value', min: range.minValue, max: range.maxValue, interval: dates ? undefined : range.increment, axisLabel: Object.assign({ show: true, color: theme.textColor, formatter: getAxisValueFormatter(range, settings, dates || undefined) }, ticks) }, (ticks && { axisTick: ticks })), { axisLine: { lineStyle: { color: theme.axisLineColor } }, splitLine: { lineStyle: { color: theme.axisLineColor } } }), name);
536
548
  }
537
549
  function getAxisName(settings, name, theme) {
538
550
  if (!settings.displayAxisLabels) {
@@ -545,7 +557,7 @@ function getAxisName(settings, name, theme) {
545
557
  };
546
558
  }
547
559
  function getValueLabel(settings, theme) {
548
- return Object.assign(Object.assign({ show: settings.displayItemValue, position: settings.orientation === 'landscape' ? 'top' : 'right' }, getLabelStyle(theme)), { formatter: ({ dataIndex }) => formatValue(settings.items[dataIndex], settings.type) });
560
+ return Object.assign(Object.assign({ show: settings.displayItemValue, position: settings.orientation === 'landscape' ? 'top' : 'right' }, getLabelStyle(theme)), { formatter: ({ dataIndex }) => formatValue(settings.items[dataIndex], settings) });
549
561
  }
550
562
  function getLabelStyle(theme) {
551
563
  return { color: theme.textColor, textBorderWidth: 0 };
@@ -556,7 +568,7 @@ function getPointLabel(settings, item) {
556
568
  parts.push(item.text);
557
569
  }
558
570
  if (settings.displayItemValue) {
559
- parts.push(formatValue(item, settings.type));
571
+ parts.push(formatValue(item, settings));
560
572
  }
561
573
  return parts.join(' ');
562
574
  }
@@ -1,5 +1,6 @@
1
1
  const PERCENT = 100;
2
2
  const STEP_COUNT = 10;
3
+ const DAY = 24 * 60 * 60 * 1000;
3
4
  /**
4
5
  * The most steps that an `axisIncrement` may divide a value axis into.
5
6
  */
@@ -102,6 +103,114 @@ export function getAxisRange(items, maxValue, axisIncrement) {
102
103
  : getNiceStep(span);
103
104
  return roundOutwards(minRange, finalMaxRange, increment);
104
105
  }
106
+ /**
107
+ * Whether `value` is a date in milliseconds that `Date` can hold. A date
108
+ * in nanoseconds is not, and neither is a date given as text.
109
+ * @param value - the value to check
110
+ */
111
+ export function isDate(value) {
112
+ return (typeof value === 'number' && Number.isFinite(new Date(value).getTime()));
113
+ }
114
+ /**
115
+ * The range of an axis of dates, in milliseconds: from the start of the
116
+ * earliest day to the latest date, or to `maxValue` when that is set, as an
117
+ * axis of numbers ends at `maxValue`. An end during a day is moved to the
118
+ * end of that day. Unlike an axis of numbers, it does not reach down to
119
+ * zero, which would be 1970, and values that are not dates are left out.
120
+ * Its increment is a day, though `getDateTicks` decides what the axis
121
+ * marks.
122
+ * @param values - the dates on the axis
123
+ * @param maxValue - the requested upper bound, if any
124
+ */
125
+ export function getDateRange(values, maxValue) {
126
+ const dates = values.filter((value) => isDate(value));
127
+ const start = new Date(dates.length > 0 ? Math.min(...dates) : Date.now());
128
+ start.setHours(0, 0, 0, 0);
129
+ const end = new Date(isDate(maxValue) ? maxValue : Math.max(start.getTime(), ...dates));
130
+ if (end.getTime() <= start.getTime()) {
131
+ end.setTime(addDays(start, 1).getTime());
132
+ }
133
+ else if (end.getTime() > startOfDay(end)) {
134
+ end.setTime(addDays(new Date(startOfDay(end)), 1).getTime());
135
+ }
136
+ return {
137
+ minValue: start.getTime(),
138
+ maxValue: end.getTime(),
139
+ totalRange: end.getTime() - start.getTime(),
140
+ increment: DAY,
141
+ };
142
+ }
143
+ const DAYS_IN_MONTH = 30.44;
144
+ const DAYS_IN_YEAR = 365.25;
145
+ const DATE_STEP_COUNT = 6;
146
+ const dayStep = (count) => ({
147
+ unit: 'day',
148
+ days: count,
149
+ first: (date) => date,
150
+ next: (date) => addDays(date, count),
151
+ });
152
+ const weekStep = (count) => ({
153
+ unit: 'day',
154
+ days: 7 * count,
155
+ first: (date) => addDays(date, (8 - date.getDay()) % 7),
156
+ next: (date) => addDays(date, 7 * count),
157
+ });
158
+ const monthStep = (count) => ({
159
+ unit: 'month',
160
+ days: DAYS_IN_MONTH * count,
161
+ first: (date) => {
162
+ let month = date.getDate() === 1 ? date : getFirstOfMonth(date, 1);
163
+ while (month.getMonth() % count !== 0) {
164
+ month = getFirstOfMonth(month, 1);
165
+ }
166
+ return month;
167
+ },
168
+ next: (date) => getFirstOfMonth(date, count),
169
+ });
170
+ const yearStep = (count) => ({
171
+ unit: 'year',
172
+ days: DAYS_IN_YEAR * count,
173
+ first: (date) => {
174
+ let year = date.getFullYear();
175
+ if (date.getTime() > createDate(year, 0, 1).getTime()) {
176
+ year += 1;
177
+ }
178
+ return createDate(Math.ceil(year / count) * count, 0, 1);
179
+ },
180
+ next: (date) => createDate(date.getFullYear() + count, 0, 1),
181
+ });
182
+ const DATE_STEPS = [
183
+ dayStep(1),
184
+ dayStep(2),
185
+ weekStep(1),
186
+ weekStep(2),
187
+ monthStep(1),
188
+ monthStep(3),
189
+ monthStep(6),
190
+ yearStep(1),
191
+ yearStep(2),
192
+ yearStep(5),
193
+ yearStep(10),
194
+ ];
195
+ /**
196
+ * The dates to mark on an axis of dates: days, Mondays, or the first days
197
+ * of months, quarters, half years or years, whichever divides the axis
198
+ * into at most about six steps. Dates take more room than numbers, so
199
+ * they get fewer steps.
200
+ * @param range - the range of the axis, from `getDateRange`
201
+ * @returns the dates to mark, and whether they are days, months or years
202
+ */
203
+ export function getDateTicks(range) {
204
+ var _a;
205
+ const days = (range.maxValue - range.minValue) / DAY;
206
+ const decades = days / DAYS_IN_YEAR / 10;
207
+ const step = (_a = DATE_STEPS.find((candidate) => days / candidate.days <= DATE_STEP_COUNT)) !== null && _a !== void 0 ? _a : yearStep(10 * Math.ceil(decades / DATE_STEP_COUNT));
208
+ const ticks = [];
209
+ for (let date = step.first(new Date(range.minValue)); date.getTime() <= range.maxValue; date = step.next(date)) {
210
+ ticks.push(date.getTime());
211
+ }
212
+ return { values: ticks, unit: step.unit };
213
+ }
105
214
  /**
106
215
  * The range of a chart whose items are parts of one whole: `maxValue`
107
216
  * when set, otherwise the combined item values or range extents. The value
@@ -144,25 +253,6 @@ export function getScatterRanges(items) {
144
253
  export function getShare(item, whole) {
145
254
  return (getValueSpan(item) / whole.totalRange) * PERCENT;
146
255
  }
147
- /**
148
- * The item's value as text: its `formattedValue` when set, otherwise its
149
- * value, range, or scatter point.
150
- * @param item - the item
151
- * @param type - the chart type
152
- */
153
- export function formatValue(item, type) {
154
- const { value, formattedValue } = item;
155
- if (formattedValue) {
156
- return formattedValue;
157
- }
158
- if (Array.isArray(value)) {
159
- if (type === 'scatter') {
160
- return `(${value[0]}, ${value[1]})`;
161
- }
162
- return `${value[0]} — ${value[1]}`;
163
- }
164
- return `${value}`;
165
- }
166
256
  function isUsableStep(step, span) {
167
257
  return step > 0 && span / step <= MAX_AXIS_STEPS;
168
258
  }
@@ -181,6 +271,29 @@ function getZeroAnchoredRange(values) {
181
271
  const maxValue = Math.max(0, ...values);
182
272
  return roundOutwards(minValue, maxValue, getNiceStep(maxValue - minValue));
183
273
  }
274
+ function addDays(date, days) {
275
+ return createDate(date.getFullYear(), date.getMonth(), date.getDate() + days);
276
+ }
277
+ function getFirstOfMonth(date, monthsLater) {
278
+ return createDate(date.getFullYear(), date.getMonth() + monthsLater, 1);
279
+ }
280
+ /**
281
+ * The start of a day in local time. Unlike `new Date(year, month, day)`, it
282
+ * keeps the years 0 to 99 rather than reading them as 1900 to 1999, see
283
+ * https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-makefullyear
284
+ * @param year - the full year
285
+ * @param month - the month, from 0 for January
286
+ * @param day - the day of the month
287
+ */
288
+ function createDate(year, month, day) {
289
+ const date = new Date(0);
290
+ date.setFullYear(year, month, day);
291
+ date.setHours(0, 0, 0, 0);
292
+ return date;
293
+ }
294
+ function startOfDay(date) {
295
+ return new Date(date).setHours(0, 0, 0, 0);
296
+ }
184
297
  function roundOutwards(minValue, maxValue, increment) {
185
298
  const lowerEnd = Math.floor(minValue / increment) * increment;
186
299
  const upperEnd = Math.ceil(maxValue / increment) * increment;
@@ -524,10 +524,20 @@ export class InputField {
524
524
  this.showCompletions = false;
525
525
  return;
526
526
  }
527
+ if (event.key === ARROW_UP && this.isKeyPressedOnFirstOption(event)) {
528
+ event.preventDefault();
529
+ this.setFocus();
530
+ return;
531
+ }
527
532
  if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
528
533
  this.setFocus();
529
534
  }
530
535
  };
536
+ this.isKeyPressedOnFirstOption = (event) => {
537
+ const list = event.currentTarget;
538
+ const firstOption = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
539
+ return !!firstOption && event.composedPath().includes(firstOption);
540
+ };
531
541
  this.handleCloseMenu = () => {
532
542
  this.showCompletions = false;
533
543
  };
@@ -1,7 +1,7 @@
1
1
  import { h, Host, } from "@stencil/core";
2
2
  import translate from "../../global/translations";
3
3
  import { createRandomString } from "../../util/random-string";
4
- import { getPrimarySubtag } from "../../util/language";
4
+ import { getSupportedLocale } from "../../util/language";
5
5
  import { getPageSlots } from "./pagination.util";
6
6
  const FIRST_PAGE = 1;
7
7
  const DEFAULT_PAGE_SIZE = 100;
@@ -304,7 +304,7 @@ export class Pagination {
304
304
  }
305
305
  render() {
306
306
  const { slots } = this.view;
307
- return (h(Host, { key: '924becb588f7d773572b61865b9dd79b14171153' }, h("nav", { key: 'f517b62cf0107c912d576f08e9d99594f868be44', "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: '21758520849acfe32f43e86570fe60a040cf2748', class: "live-region", "aria-live": "polite" }, this.liveMessage)));
307
+ return (h(Host, { key: '7c47cdc658859b4cd8199ef92ebd9bc48275a8ae' }, h("nav", { key: '3bba45547a7183a4b1df58294a56788f9b2e078f', "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: '60fff33d8e135b1605d0a0f68b6c1ede1b571281', class: "live-region", "aria-live": "polite" }, this.liveMessage)));
308
308
  }
309
309
  /**
310
310
  * Without a count we know where the user is but not how far the set goes,
@@ -620,9 +620,7 @@ export class Pagination {
620
620
  }
621
621
  /**
622
622
  * What the jump form is given, so that the one warning about a language
623
- * this browser cannot write numbers in describes both of us. A tag it
624
- * accepts but does not support, `zz`, resolves to the viewer's own locale
625
- * rather than throwing, so asking the formatter is the only way to know.
623
+ * this browser cannot write numbers in describes both of us.
626
624
  */
627
625
  get languageForNumbers() {
628
626
  var _a;
@@ -630,31 +628,21 @@ export class Pagination {
630
628
  return this.settledLanguage;
631
629
  }
632
630
  /**
633
- * `Intl.NumberFormat` throws on a language it cannot parse, and this is
634
- * called from `render`, so an unusable one would leave the control drawing
635
- * nothing at all — the single state a user cannot click their way out of.
636
- * `src/util/language.ts` states the rule this keeps.
637
- *
638
- * A tag it cannot parse throws; a well-formed tag it does not support, like
639
- * `zz`, does not — it quietly resolves to whatever locale the machine runs
640
- * in. Both end up formatting numbers in something other than what was
641
- * asked for, so both are worth saying out loud.
631
+ * This is called from `render`, so a language that `Intl` cannot write
632
+ * numbers in must not throw, or the control would draw nothing at all —
633
+ * the single state a user cannot click their way out of.
634
+ * `getSupportedLocale` in `src/util/language.ts` decides which languages
635
+ * those are.
642
636
  *
643
637
  * @returns a formatter, in the given language where that is possible
644
638
  */
645
639
  createNumberFormat() {
646
640
  const { language } = this;
647
- try {
648
- const format = new Intl.NumberFormat(language);
649
- const resolved = format.resolvedOptions().locale;
650
- if (getPrimarySubtag(resolved) === getPrimarySubtag(language)) {
651
- this.warnedAbout.delete('language');
652
- this.settledLanguage = language;
653
- return format;
654
- }
655
- }
656
- catch (_a) {
657
- // Fall through to the warning below.
641
+ const locale = getSupportedLocale(language);
642
+ if (locale) {
643
+ this.warnedAbout.delete('language');
644
+ this.settledLanguage = language;
645
+ return new Intl.NumberFormat(locale);
658
646
  }
659
647
  this.warnOnce('language', language, () => `\`language\` must be one this browser can write numbers in, but was ${JSON.stringify(language)}. Using ${FALLBACK_LANGUAGE}.`);
660
648
  // Where the translations land for the same unusable value, rather than
@@ -10,6 +10,8 @@ import { debounce } from "lodash-es";
10
10
  const SEARCH_DEBOUNCE = 300;
11
11
  const CHIP_SET_TAG_NAME = 'limel-chip-set';
12
12
  const DEFAULT_SEARCHER_MAX_RESULTS = 20;
13
+ const MAX_FRAMES_TO_WAIT_FOR_LIST = 20;
14
+ const ENABLED_LIST_ITEM = '.mdc-deprecated-list-item:not(.mdc-deprecated-list-item--disabled)';
13
15
  /**
14
16
  * @exampleComponent limel-example-picker-basic
15
17
  * @exampleComponent limel-example-picker-multiple
@@ -98,7 +100,8 @@ export class Picker {
98
100
  this.chips = [];
99
101
  // Should NOT be decorated with State(), since this
100
102
  // should not trigger a re-render by itself.
101
- this.chipSetEditMode = false;
103
+ this.isEditing = false;
104
+ this.suggestionHasFocus = false;
102
105
  this.suggestionsMessage = createLiveMessage(() => this.getSuggestionsMessage());
103
106
  this.getValueId = (item) => {
104
107
  const value = item.value;
@@ -182,6 +185,7 @@ export class Picker {
182
185
  this.suggestionsMessage.update();
183
186
  }
184
187
  disconnectedCallback() {
188
+ this.forgetSuggestionFocus();
185
189
  this.debouncedSearch.cancel();
186
190
  this.suggestionsMessage.update.cancel();
187
191
  }
@@ -196,9 +200,10 @@ export class Picker {
196
200
  this.host.setAttribute('tabindex', '0');
197
201
  }
198
202
  async componentWillUpdate() {
199
- this.chipSetEditMode = false;
203
+ this.isEditing = false;
200
204
  if (this.chipSet) {
201
- this.chipSetEditMode = await this.chipSet.getEditMode();
205
+ this.isEditing =
206
+ this.suggestionHasFocus || (await this.chipSet.getEditMode());
202
207
  }
203
208
  }
204
209
  render() {
@@ -207,7 +212,7 @@ export class Picker {
207
212
  if (!this.multiple) {
208
213
  props.maxItems = 1;
209
214
  }
210
- return (h(Host, { key: 'b8a2414331b89aa646ecdab2a0e5fb9bfce06de4' }, h("limel-chip-set", Object.assign({ key: 'bf4fe7ed7973f40c903116cd538f485e3f44881d', type: "input", inputType: "search", label: this.label, helperText: this.helperText, leadingIcon: this.leadingIcon, value: this.chips, disabled: this.disabled, invalid: this.invalid, delimiter: this.renderDelimiter(), readonly: this.readonly, required: this.required, searchLabel: this.searchLabel, language: this.language, onInput: this.handleTextInput, onKeyDown: this.handleInputKeyDown, onChange: this.handleChange, onInteract: this.handleInteract, onStartEdit: this.handleInputFieldFocus, onStopEdit: this.handleStopEditAndBlur, emptyInputOnBlur: false, emptyInputOnChange: false, clearAllButton: this.multiple && !this.chipSetEditMode, suggestionListId: this.canOpenList() ? this.portalId : undefined, suggestionListOpen: this.suggestionsEngaged && dropdownContent.length > 0 }, props)), this.renderPortal(dropdownContent), h("span", { key: '3967fc14d3207567c5976c48c1154b96ea3fe285', class: "suggestions-message", "aria-live": "polite", ref: this.suggestionsMessage.setElement })));
215
+ return (h(Host, { key: '1670bff5822f930a3eca69ad2a016cea978b3822' }, h("limel-chip-set", Object.assign({ key: '173a7facd5271992e792ce6a56a34a8890c9135f', type: "input", inputType: "search", label: this.label, helperText: this.helperText, leadingIcon: this.leadingIcon, value: this.chips, disabled: this.disabled, invalid: this.invalid, delimiter: this.renderDelimiter(), readonly: this.readonly, required: this.required, searchLabel: this.searchLabel, language: this.language, onInput: this.handleTextInput, onKeyDown: this.handleInputKeyDown, onChange: this.handleChange, onInteract: this.handleInteract, onStartEdit: this.handleInputFieldFocus, onStopEdit: this.handleStopEditAndBlur, emptyInputOnBlur: false, emptyInputOnChange: false, clearAllButton: this.multiple && !this.isEditing, suggestionListId: this.canOpenList() ? this.portalId : undefined, suggestionListOpen: this.suggestionsEngaged && dropdownContent.length > 0 }, props)), this.renderPortal(dropdownContent), h("span", { key: 'c151104c5059aadc3cfa526267184b663e87bebd', class: "suggestions-message", "aria-live": "polite", ref: this.suggestionsMessage.setElement })));
211
216
  }
212
217
  onChangeValue() {
213
218
  this.chips = this.createChips(this.value);
@@ -280,7 +285,7 @@ export class Picker {
280
285
  return !this.readonly && !this.isFull();
281
286
  }
282
287
  shouldShowDropDownContent() {
283
- return this.canOpenList() && !!this.chipSetEditMode;
288
+ return this.canOpenList() && !!this.isEditing;
284
289
  }
285
290
  getDropdownContent() {
286
291
  var _a;
@@ -349,10 +354,20 @@ export class Picker {
349
354
  this.chipSet.setFocus();
350
355
  return;
351
356
  }
357
+ if (event.key === ARROW_UP && this.isKeyPressedOnFirstItem(event)) {
358
+ event.preventDefault();
359
+ this.chipSet.setFocus();
360
+ return;
361
+ }
352
362
  if ([TAB, ENTER].includes(event.key) || isTextEditingKey(event)) {
353
363
  this.chipSet.setFocus();
354
364
  }
355
365
  }
366
+ isKeyPressedOnFirstItem(event) {
367
+ const list = event.currentTarget;
368
+ const firstItem = list.shadowRoot.querySelector(ENABLED_LIST_ITEM);
369
+ return !!firstItem && event.composedPath().includes(firstItem);
370
+ }
356
371
  renderPortal(content = []) {
357
372
  const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
358
373
  return (h("limel-portal", { visible: content.length > 0, containerId: this.portalId, inheritParentWidth: true, containerStyle: { 'z-index': dropdownZIndex } }, h("limel-menu-surface", { open: content.length > 0, allowClicksElement: this.host, style: {
@@ -386,6 +401,7 @@ export class Picker {
386
401
  const query = event.detail;
387
402
  this.textValue = query;
388
403
  this.suggestionsEngaged = true;
404
+ this.forgetSuggestionFocus();
389
405
  this.debouncedSearch(query);
390
406
  // If the search-query is an empty string, bypass debouncing.
391
407
  if (query === '') {
@@ -435,6 +451,7 @@ export class Picker {
435
451
  if (!event.detail) {
436
452
  return;
437
453
  }
454
+ this.suggestionHasFocus = false;
438
455
  this.action.emit(event.detail.value);
439
456
  this.items = [];
440
457
  }
@@ -443,6 +460,7 @@ export class Picker {
443
460
  * Prevent focus if the picker has a value and does not support multiple values
444
461
  */
445
462
  handleInputFieldFocus() {
463
+ this.forgetSuggestionFocus();
446
464
  const query = this.textValue;
447
465
  this.debouncedSearch(query);
448
466
  }
@@ -480,7 +498,6 @@ export class Picker {
480
498
  * @param event - event
481
499
  */
482
500
  handleInputKeyDown(event) {
483
- var _a;
484
501
  this.suggestionsEngaged = true;
485
502
  const isUp = event.key === ARROW_UP;
486
503
  const isDown = event.key === ARROW_DOWN;
@@ -494,28 +511,47 @@ export class Picker {
494
511
  if (!isUp && !isDown) {
495
512
  return;
496
513
  }
514
+ const position = isDown ? 'first' : 'last';
515
+ if (this.focusSuggestion(position)) {
516
+ event.preventDefault();
517
+ return;
518
+ }
519
+ this.suggestionToFocusWhenShown = position;
520
+ }
521
+ focusSuggestion(position) {
522
+ var _a;
497
523
  const list = (_a = document.querySelector(` #${this.portalId} limel-list[data-suggestions]`)) !== null && _a !== void 0 ? _a : document.querySelector(` #${this.portalId} limel-list`);
498
524
  if (!list) {
499
- return;
525
+ return false;
500
526
  }
501
- if (isDown) {
502
- // Match by class only (not `:first-child`) so a leading
503
- // `ListSeparator` doesn't shadow the first focusable item.
504
- const listElement = list.shadowRoot.querySelector('.mdc-deprecated-list-item');
505
- if (!listElement) {
527
+ // Match by class only (not `:first-child`) so a leading
528
+ // `ListSeparator` doesn't shadow the first focusable item.
529
+ const items = list.shadowRoot.querySelectorAll(ENABLED_LIST_ITEM);
530
+ const item = position === 'first' ? items[0] : [...items].at(-1);
531
+ item === null || item === void 0 ? void 0 : item.focus();
532
+ this.suggestionHasFocus =
533
+ !!item && list.shadowRoot.activeElement === item;
534
+ return this.suggestionHasFocus;
535
+ }
536
+ focusSuggestionWhenShown(position) {
537
+ cancelAnimationFrame(this.suggestionFocusFrame);
538
+ let frames = 0;
539
+ const focusSuggestion = () => {
540
+ if (this.focusSuggestion(position)) {
506
541
  return;
507
542
  }
508
- event.preventDefault();
509
- listElement.focus();
510
- return;
511
- }
512
- const listItems = list.shadowRoot.querySelectorAll('.mdc-deprecated-list-item');
513
- const listElement = [...listItems].at(-1);
514
- if (!listElement) {
515
- return;
516
- }
517
- event.preventDefault();
518
- listElement.focus();
543
+ frames += 1;
544
+ if (frames < MAX_FRAMES_TO_WAIT_FOR_LIST) {
545
+ this.suggestionFocusFrame =
546
+ requestAnimationFrame(focusSuggestion);
547
+ }
548
+ };
549
+ this.suggestionFocusFrame = requestAnimationFrame(focusSuggestion);
550
+ }
551
+ forgetSuggestionFocus() {
552
+ this.suggestionHasFocus = false;
553
+ this.suggestionToFocusWhenShown = undefined;
554
+ cancelAnimationFrame(this.suggestionFocusFrame);
519
555
  }
520
556
  handleSearchResult(query, result) {
521
557
  var _a;
@@ -534,6 +570,11 @@ export class Picker {
534
570
  }
535
571
  this.items = this.prependSearchHeader(query, nextItems);
536
572
  this.loading = false;
573
+ const position = this.suggestionToFocusWhenShown;
574
+ this.suggestionToFocusWhenShown = undefined;
575
+ if (position && this.hasPickableItems(nextItems)) {
576
+ this.focusSuggestionWhenShown(position);
577
+ }
537
578
  }
538
579
  hasPickableItems(items) {
539
580
  return items.some((item) => !('separator' in item));
@@ -556,7 +597,8 @@ export class Picker {
556
597
  /**
557
598
  * Shared prelude for any flow that ends the current search session:
558
599
  * wipe the chip-set's visible text, reset the picker's `textValue`,
559
- * and cancel any in-flight debounced search.
600
+ * cancel any in-flight debounced search, and forget that a suggestion
601
+ * has the focus, or that an arrow key is waiting for the suggestions.
560
602
  *
561
603
  * Used by `clearInputField` (which then drops the dropdown
562
604
  * entirely) and `resetSearchToDefault` (which re-runs the searcher
@@ -566,6 +608,7 @@ export class Picker {
566
608
  this.chipSet.emptyInput();
567
609
  this.textValue = '';
568
610
  this.debouncedSearch.cancel();
611
+ this.forgetSuggestionFocus();
569
612
  }
570
613
  clearInputField() {
571
614
  this.clearTextValue();
@@ -22,3 +22,32 @@ export function getPrimarySubtag(language) {
22
22
  }
23
23
  return language.toLowerCase().split('-', 1)[0];
24
24
  }
25
+ /**
26
+ * The locale that `Intl` writes numbers and dates in for `language`, or
27
+ * `undefined` when it cannot write in that language. `Intl` throws on a
28
+ * tag it cannot read, such as `sv_SE`, and quietly uses the viewer's own
29
+ * locale for one it does not know, such as `zz`. A component that writes in
30
+ * its `language` while it renders checks it here, and falls back to English
31
+ * when this gives `undefined`.
32
+ *
33
+ * Examples:
34
+ * - "sv" => "sv"
35
+ * - "SV-se" => "sv-SE"
36
+ * - "sv_SE" => undefined
37
+ * - "zz" => undefined
38
+ * - undefined => undefined
39
+ *
40
+ * @param language - the language to write in
41
+ * @returns the locale, or `undefined`
42
+ */
43
+ export function getSupportedLocale(language) {
44
+ if (typeof language !== 'string') {
45
+ return;
46
+ }
47
+ try {
48
+ return Intl.NumberFormat.supportedLocalesOf(language)[0];
49
+ }
50
+ catch (_a) {
51
+ return;
52
+ }
53
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Reduce a language to the primary subtag that our language maps are keyed by.
3
+ *
4
+ * A `language` prop is typed, but a custom element takes whatever string an
5
+ * attribute carries — commonly a full BCP 47 tag such as `nb-NO`, since a host
6
+ * application often derives the language from `document.documentElement.lang`.
7
+ * Anything that is not a string must resolve rather than throw, so that an
8
+ * unexpected value can never break a component's render.
9
+ *
10
+ * Examples:
11
+ * - "nb" => "nb"
12
+ * - "nb-NO" => "nb"
13
+ * - "SV-SE" => "sv"
14
+ * - undefined => ""
15
+ *
16
+ * @param language - the language to reduce
17
+ * @returns the lower-cased primary subtag, or an empty string
18
+ */
19
+ function getPrimarySubtag(language) {
20
+ if (typeof language !== 'string') {
21
+ return '';
22
+ }
23
+ return language.toLowerCase().split('-', 1)[0];
24
+ }
25
+ /**
26
+ * The locale that `Intl` writes numbers and dates in for `language`, or
27
+ * `undefined` when it cannot write in that language. `Intl` throws on a
28
+ * tag it cannot read, such as `sv_SE`, and quietly uses the viewer's own
29
+ * locale for one it does not know, such as `zz`. A component that writes in
30
+ * its `language` while it renders checks it here, and falls back to English
31
+ * when this gives `undefined`.
32
+ *
33
+ * Examples:
34
+ * - "sv" => "sv"
35
+ * - "SV-se" => "sv-SE"
36
+ * - "sv_SE" => undefined
37
+ * - "zz" => undefined
38
+ * - undefined => undefined
39
+ *
40
+ * @param language - the language to write in
41
+ * @returns the locale, or `undefined`
42
+ */
43
+ function getSupportedLocale(language) {
44
+ if (typeof language !== 'string') {
45
+ return;
46
+ }
47
+ try {
48
+ return Intl.NumberFormat.supportedLocalesOf(language)[0];
49
+ }
50
+ catch (_a) {
51
+ return;
52
+ }
53
+ }
54
+
55
+ export { getPrimarySubtag as a, getSupportedLocale as g };