@lion/ui 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/components/calendar/src/LionCalendar.js +1 -1
  2. package/components/calendar/test/lion-calendar.test.js +67 -47
  3. package/components/calendar/test-suites/LionCalendar.suite.js +79 -0
  4. package/components/form-core/src/choice-group/ChoiceInputMixin.js +1 -1
  5. package/components/input-amount-dropdown/src/LionInputAmountDropdown.js +6 -4
  6. package/components/input-amount-dropdown/test/LionInputAmountDropdown.test.js +20 -1
  7. package/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.js +24 -0
  8. package/components/input-datepicker/test/lion-input-datepicker.test.js +13 -4
  9. package/components/input-datepicker/test-suites/LionInputDatepicker.suite.js +35 -0
  10. package/components/input-email/src/LionInputEmail.js +2 -0
  11. package/components/input-email/test/lion-input-email.test.js +6 -0
  12. package/components/input-file/src/validators.js +31 -1
  13. package/components/input-file/test/FileValidators.test.js +31 -0
  14. package/components/input-file/translations/bg.js +1 -0
  15. package/components/input-file/translations/cs.js +1 -0
  16. package/components/input-file/translations/de.js +2 -0
  17. package/components/input-file/translations/en.js +1 -0
  18. package/components/input-file/translations/es.js +1 -0
  19. package/components/input-file/translations/fr.js +1 -0
  20. package/components/input-file/translations/hu.js +1 -0
  21. package/components/input-file/translations/id.js +1 -0
  22. package/components/input-file/translations/it.js +1 -0
  23. package/components/input-file/translations/nl.js +1 -0
  24. package/components/input-file/translations/pl.js +1 -0
  25. package/components/input-file/translations/ro.js +1 -0
  26. package/components/input-file/translations/ru.js +1 -0
  27. package/components/input-file/translations/sk.js +1 -0
  28. package/components/input-file/translations/uk.js +1 -0
  29. package/components/input-file/translations/zh.js +1 -0
  30. package/components/input-tel/src/LionInputTel.js +1 -1
  31. package/components/input-tel/test-suites/LionInputTel.suite.js +7 -0
  32. package/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.js +132 -26
  33. package/components/overlays/test/withHoverInteraction.test.js +118 -0
  34. package/components/switch/src/LionSwitch.js +1 -1
  35. package/components/switch/test/lion-switch.test.js +7 -0
  36. package/custom-elements.json +403 -62
  37. package/dist-types/components/calendar/test/lion-calendar.test.d.ts.map +1 -1
  38. package/dist-types/components/calendar/test-suites/LionCalendar.suite.d.ts +5 -0
  39. package/dist-types/components/calendar/test-suites/LionCalendar.suite.d.ts.map +1 -0
  40. package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts +0 -9
  41. package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts.map +1 -1
  42. package/dist-types/components/input-amount-dropdown/test/LionInputAmountDropdown.test.d.ts.map +1 -1
  43. package/dist-types/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.d.ts.map +1 -1
  44. package/dist-types/components/input-datepicker/test/lion-input-datepicker.test.d.ts.map +1 -1
  45. package/dist-types/components/input-datepicker/test-suites/LionInputDatepicker.suite.d.ts +7 -0
  46. package/dist-types/components/input-datepicker/test-suites/LionInputDatepicker.suite.d.ts.map +1 -0
  47. package/dist-types/components/input-email/src/LionInputEmail.d.ts.map +1 -1
  48. package/dist-types/components/input-file/src/validators.d.ts +11 -0
  49. package/dist-types/components/input-file/src/validators.d.ts.map +1 -1
  50. package/dist-types/components/input-file/translations/bg-BG.d.ts +1 -0
  51. package/dist-types/components/input-file/translations/bg.d.ts +1 -0
  52. package/dist-types/components/input-file/translations/cs-CZ.d.ts +1 -0
  53. package/dist-types/components/input-file/translations/cs.d.ts +1 -0
  54. package/dist-types/components/input-file/translations/de-DE.d.ts +1 -0
  55. package/dist-types/components/input-file/translations/de.d.ts +1 -0
  56. package/dist-types/components/input-file/translations/en-AU.d.ts +1 -0
  57. package/dist-types/components/input-file/translations/en-GB.d.ts +1 -0
  58. package/dist-types/components/input-file/translations/en-PH.d.ts +1 -0
  59. package/dist-types/components/input-file/translations/en-US.d.ts +1 -0
  60. package/dist-types/components/input-file/translations/en.d.ts +1 -0
  61. package/dist-types/components/input-file/translations/es-ES.d.ts +1 -0
  62. package/dist-types/components/input-file/translations/es.d.ts +1 -0
  63. package/dist-types/components/input-file/translations/fr-BE.d.ts +1 -0
  64. package/dist-types/components/input-file/translations/fr-FR.d.ts +1 -0
  65. package/dist-types/components/input-file/translations/fr.d.ts +1 -0
  66. package/dist-types/components/input-file/translations/hu-HU.d.ts +1 -0
  67. package/dist-types/components/input-file/translations/hu.d.ts +1 -0
  68. package/dist-types/components/input-file/translations/id-ID.d.ts +1 -0
  69. package/dist-types/components/input-file/translations/id.d.ts +1 -0
  70. package/dist-types/components/input-file/translations/it-IT.d.ts +1 -0
  71. package/dist-types/components/input-file/translations/it.d.ts +1 -0
  72. package/dist-types/components/input-file/translations/nl-BE.d.ts +1 -0
  73. package/dist-types/components/input-file/translations/nl-NL.d.ts +1 -0
  74. package/dist-types/components/input-file/translations/nl.d.ts +1 -0
  75. package/dist-types/components/input-file/translations/pl-PL.d.ts +1 -0
  76. package/dist-types/components/input-file/translations/pl.d.ts +1 -0
  77. package/dist-types/components/input-file/translations/ro-RO.d.ts +1 -0
  78. package/dist-types/components/input-file/translations/ro.d.ts +1 -0
  79. package/dist-types/components/input-file/translations/ru-RU.d.ts +1 -0
  80. package/dist-types/components/input-file/translations/ru.d.ts +1 -0
  81. package/dist-types/components/input-file/translations/sk-SK.d.ts +1 -0
  82. package/dist-types/components/input-file/translations/sk.d.ts +1 -0
  83. package/dist-types/components/input-file/translations/uk-UA.d.ts +1 -0
  84. package/dist-types/components/input-file/translations/uk.d.ts +1 -0
  85. package/dist-types/components/input-file/translations/zh-CN.d.ts +1 -0
  86. package/dist-types/components/input-file/translations/zh.d.ts +1 -0
  87. package/dist-types/components/input-tel/src/LionInputTel.d.ts.map +1 -1
  88. package/dist-types/components/input-tel/test-suites/LionInputTel.suite.d.ts.map +1 -1
  89. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts +8 -2
  90. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts.map +1 -1
  91. package/dist-types/components/overlays/test/withHoverInteraction.test.d.ts +2 -0
  92. package/dist-types/components/overlays/test/withHoverInteraction.test.d.ts.map +1 -0
  93. package/dist-types/exports/input-file.d.ts +1 -1
  94. package/dist-types/tsconfig.tsbuildinfo +1 -1
  95. package/docs/components/checkbox-group/overview.md +2 -2
  96. package/docs/components/collapsible/overview.md +1 -1
  97. package/docs/components/combobox/overview.md +1 -1
  98. package/docs/components/combobox/use-cases.md +1 -1
  99. package/docs/components/dialog/overview.md +1 -1
  100. package/docs/components/fieldset/overview.md +5 -5
  101. package/docs/components/form/overview.md +3 -3
  102. package/docs/components/form/use-cases.md +2 -2
  103. package/docs/components/icon/use-cases.md +1 -1
  104. package/docs/components/input/overview.md +1 -1
  105. package/docs/components/input-amount/overview.md +3 -3
  106. package/docs/components/input-amount-dropdown/api-table.md +1 -1
  107. package/docs/components/input-date/overview.md +3 -3
  108. package/docs/components/input-datepicker/overview.md +5 -5
  109. package/docs/components/input-email/api-table.md +1 -1
  110. package/docs/components/input-email/overview.md +2 -2
  111. package/docs/components/input-file/overview.md +1 -1
  112. package/docs/components/input-file/use-cases.md +18 -0
  113. package/docs/components/input-iban/overview.md +2 -2
  114. package/docs/components/input-range/overview.md +2 -2
  115. package/docs/components/input-stepper/overview.md +1 -1
  116. package/docs/components/input-tel/api-table.md +1 -1
  117. package/docs/components/input-tel/overview.md +1 -1
  118. package/docs/components/input-tel/use-cases.md +6 -6
  119. package/docs/components/input-tel-dropdown/api-table.md +1 -1
  120. package/docs/components/input-tel-dropdown/overview.md +1 -1
  121. package/docs/components/listbox/use-cases.md +1 -1
  122. package/docs/components/radio-group/overview.md +3 -3
  123. package/docs/components/select/use-cases.md +1 -1
  124. package/docs/components/select-rich/use-cases.md +2 -2
  125. package/docs/components/tabs/examples.md +1 -1
  126. package/docs/components/tooltip/use-cases.md +55 -0
  127. package/docs/fundamentals/index.md +4 -4
  128. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  129. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  130. package/docs/fundamentals/rationales/accessibility.md +1 -1
  131. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  132. package/docs/fundamentals/systems/core/overview.md +1 -1
  133. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  134. package/docs/fundamentals/systems/form/model-value.md +1 -1
  135. package/docs/fundamentals/systems/form/overview.md +28 -28
  136. package/docs/fundamentals/systems/form/validate.md +2 -2
  137. package/docs/fundamentals/systems/localize/overview.md +4 -4
  138. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  139. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  140. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  141. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  142. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  143. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  144. package/docs/guides/how-to/create-a-form.md +4 -4
  145. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  146. package/docs/guides/how-to/index.md +1 -1
  147. package/docs/guides/index.md +8 -8
  148. package/docs/guides/principles/index.md +5 -5
  149. package/docs/guides/principles/styling.md +1 -1
  150. package/exports/input-file.js +5 -1
  151. package/input-file.js +5 -1
  152. package/package.json +2 -2
@@ -298,7 +298,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
298
298
  this.__contentWrapperElement.addEventListener('click', this.__boundClickDateDelegation);
299
299
  this.__contentWrapperElement.addEventListener('focus', this.__boundFocusDateDelegation);
300
300
  this.__contentWrapperElement.addEventListener('blur', this.__boundBlurDateDelegation);
301
- this.__contentWrapperElement.addEventListener('keydown', this.__boundKeyboardNavigationEvent);
301
+ this.__contentWrapperElement.addEventListener('keyup', this.__boundKeyboardNavigationEvent);
302
302
  this.__eventsAdded = true;
303
303
  }
304
304
  }
@@ -4,8 +4,10 @@ import { localizeTearDown } from '@lion/ui/localize-test-helpers.js';
4
4
  import { expect, fixture as _fixture } from '@open-wc/testing';
5
5
  import sinon from 'sinon';
6
6
  import '@lion/ui/define/lion-calendar.js';
7
+ import { sendKeys } from '@web/test-runner-commands';
7
8
  import { isSameDate } from '@lion/ui/calendar.js';
8
9
  import { CalendarObject, DayObject } from '@lion/ui/calendar-test-helpers.js';
10
+ import { runCalendarSuite } from '../test-suites/LionCalendar.suite.js';
9
11
 
10
12
  /**
11
13
  * @typedef {import('../src/LionCalendar.js').LionCalendar} LionCalendar
@@ -13,6 +15,16 @@ import { CalendarObject, DayObject } from '@lion/ui/calendar-test-helpers.js';
13
15
  */
14
16
 
15
17
  const fixture = /** @type {(arg: TemplateResult) => Promise<LionCalendar>} */ (_fixture);
18
+ /**
19
+ * @param {HTMLElement} el
20
+ * @returns {HTMLElement | null | undefined}
21
+ */
22
+ const getSelectedDateEl = el => el?.shadowRoot?.querySelector('.calendar__day-button[selected]');
23
+ /**
24
+ * @param {HTMLElement} el
25
+ * @returns {void}
26
+ */
27
+ const focusSelectedDate = el => getSelectedDateEl(el)?.focus();
16
28
 
17
29
  describe('<lion-calendar>', () => {
18
30
  const localizeManager = getLocalizeManager();
@@ -294,13 +306,14 @@ describe('<lion-calendar>', () => {
294
306
  `);
295
307
  const elObj = new CalendarObject(el);
296
308
 
297
- el.__contentWrapperElement?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft' }));
309
+ focusSelectedDate(el);
310
+ await sendKeys({ press: 'ArrowLeft' });
298
311
  await el.updateComplete;
299
312
  expect(elObj.activeMonth).to.equal('October');
300
313
  expect(elObj.activeYear).to.equal('2000');
301
314
  expect(elObj.focusedDayObj?.monthday).to.equal(11);
302
315
 
303
- el.__contentWrapperElement?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
316
+ await sendKeys({ press: 'Enter' });
304
317
  await el.updateComplete;
305
318
  expect(isSameDate(/** @type {Date} */ (el.selectedDate), new Date('2000/10/12'))).to.be.true;
306
319
  expect(dateChangedSpy.called).to.equal(false);
@@ -1120,16 +1133,14 @@ describe('<lion-calendar>', () => {
1120
1133
  expect(elObj.activeMonth).to.equal('January');
1121
1134
  expect(elObj.activeYear).to.equal('2001');
1122
1135
 
1123
- el.__contentWrapperElement?.dispatchEvent(
1124
- new KeyboardEvent('keydown', { key: 'PageUp' }),
1125
- );
1136
+ focusSelectedDate(el);
1137
+ await sendKeys({ press: 'PageUp' });
1138
+
1126
1139
  await el.updateComplete;
1127
1140
  expect(elObj.activeMonth).to.equal('December');
1128
1141
  expect(elObj.activeYear).to.equal('2000');
1129
1142
 
1130
- el.__contentWrapperElement?.dispatchEvent(
1131
- new KeyboardEvent('keydown', { key: 'PageDown' }),
1132
- );
1143
+ await sendKeys({ press: 'PageDown' });
1133
1144
  await el.updateComplete;
1134
1145
  expect(elObj.activeMonth).to.equal('January');
1135
1146
  expect(elObj.activeYear).to.equal('2001');
@@ -1139,20 +1150,36 @@ describe('<lion-calendar>', () => {
1139
1150
  const el = await fixture(html`
1140
1151
  <lion-calendar .selectedDate="${new Date('2001/01/02')}"></lion-calendar>
1141
1152
  `);
1153
+ focusSelectedDate(el);
1154
+
1142
1155
  const elObj = new CalendarObject(el);
1143
1156
  expect(elObj.activeMonth).to.equal('January');
1144
1157
  expect(elObj.activeYear).to.equal('2001');
1145
1158
 
1146
- el.__contentWrapperElement?.dispatchEvent(
1147
- new KeyboardEvent('keydown', { key: 'PageDown', altKey: true }),
1148
- );
1159
+ await sendKeys({
1160
+ down: 'Alt',
1161
+ });
1162
+ await sendKeys({
1163
+ press: 'PageDown',
1164
+ });
1165
+ await sendKeys({
1166
+ up: 'Alt',
1167
+ });
1168
+
1149
1169
  await el.updateComplete;
1170
+ focusSelectedDate(el);
1150
1171
  expect(elObj.activeMonth).to.equal('January');
1151
1172
  expect(elObj.activeYear).to.equal('2002');
1152
1173
 
1153
- el.__contentWrapperElement?.dispatchEvent(
1154
- new KeyboardEvent('keydown', { key: 'PageUp', altKey: true }),
1155
- );
1174
+ await sendKeys({
1175
+ down: 'Alt',
1176
+ });
1177
+ await sendKeys({
1178
+ press: 'PageUp',
1179
+ });
1180
+ await sendKeys({
1181
+ up: 'Alt',
1182
+ });
1156
1183
  await el.updateComplete;
1157
1184
  expect(elObj.activeMonth).to.equal('January');
1158
1185
  expect(elObj.activeYear).to.equal('2001');
@@ -1165,9 +1192,8 @@ describe('<lion-calendar>', () => {
1165
1192
  `);
1166
1193
  const elObj = new CalendarObject(el);
1167
1194
 
1168
- el.__contentWrapperElement?.dispatchEvent(
1169
- new KeyboardEvent('keydown', { key: 'ArrowDown' }),
1170
- );
1195
+ focusSelectedDate(el);
1196
+ await sendKeys({ press: 'ArrowDown' });
1171
1197
  await el.updateComplete;
1172
1198
  expect(elObj.focusedDayObj?.monthday).to.equal(2 + 7);
1173
1199
  });
@@ -1178,9 +1204,8 @@ describe('<lion-calendar>', () => {
1178
1204
  `);
1179
1205
  const elObj = new CalendarObject(el);
1180
1206
 
1181
- el.__contentWrapperElement?.dispatchEvent(
1182
- new KeyboardEvent('keydown', { key: 'ArrowUp' }),
1183
- );
1207
+ focusSelectedDate(el);
1208
+ await sendKeys({ press: 'ArrowUp' });
1184
1209
  await el.updateComplete;
1185
1210
  expect(elObj.focusedDayObj?.monthday).to.equal(26); // of month before
1186
1211
  });
@@ -1192,9 +1217,8 @@ describe('<lion-calendar>', () => {
1192
1217
  `);
1193
1218
  const elObj = new CalendarObject(el);
1194
1219
 
1195
- el.__contentWrapperElement?.dispatchEvent(
1196
- new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
1197
- );
1220
+ focusSelectedDate(el);
1221
+ await sendKeys({ press: 'ArrowLeft' });
1198
1222
  await el.updateComplete;
1199
1223
  expect(elObj.focusedDayObj?.monthday).to.equal(12 - 1);
1200
1224
  });
@@ -1206,9 +1230,8 @@ describe('<lion-calendar>', () => {
1206
1230
  `);
1207
1231
  const elObj = new CalendarObject(el);
1208
1232
 
1209
- el.__contentWrapperElement?.dispatchEvent(
1210
- new KeyboardEvent('keydown', { key: 'ArrowRight' }),
1211
- );
1233
+ focusSelectedDate(el);
1234
+ await sendKeys({ press: 'ArrowRight' });
1212
1235
  await el.updateComplete;
1213
1236
  expect(elObj.focusedDayObj?.monthday).to.equal(12 + 1);
1214
1237
  });
@@ -1224,9 +1247,8 @@ describe('<lion-calendar>', () => {
1224
1247
  `);
1225
1248
  const elObj = new CalendarObject(el);
1226
1249
 
1227
- el.__contentWrapperElement?.dispatchEvent(
1228
- new KeyboardEvent('keydown', { key: 'ArrowRight' }),
1229
- );
1250
+ focusSelectedDate(el);
1251
+ await sendKeys({ press: 'ArrowRight' });
1230
1252
  await el.updateComplete;
1231
1253
  expect(elObj.focusedDayObj?.monthday).to.equal(3);
1232
1254
  });
@@ -1238,9 +1260,8 @@ describe('<lion-calendar>', () => {
1238
1260
  const elObj = new CalendarObject(el);
1239
1261
  expect(elObj.centralDayObj?.weekdayNameShort).to.equal('Sat');
1240
1262
 
1241
- el.__contentWrapperElement?.dispatchEvent(
1242
- new KeyboardEvent('keydown', { key: 'ArrowRight' }),
1243
- );
1263
+ focusSelectedDate(el);
1264
+ await sendKeys({ press: 'ArrowRight' });
1244
1265
  await el.updateComplete;
1245
1266
  expect(elObj.focusedDayObj?.monthday).to.equal(6);
1246
1267
  expect(elObj.focusedDayObj?.weekdayNameShort).to.equal('Sun');
@@ -1253,9 +1274,8 @@ describe('<lion-calendar>', () => {
1253
1274
  const elObj = new CalendarObject(el);
1254
1275
  expect(elObj.centralDayObj?.weekdayNameShort).to.equal('Sun');
1255
1276
 
1256
- el.__contentWrapperElement?.dispatchEvent(
1257
- new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
1258
- );
1277
+ focusSelectedDate(el);
1278
+ await sendKeys({ press: 'ArrowLeft' });
1259
1279
  await el.updateComplete;
1260
1280
  expect(elObj.focusedDayObj?.monthday).to.equal(5);
1261
1281
  expect(elObj.focusedDayObj?.weekdayNameShort).to.equal('Sat');
@@ -1269,9 +1289,8 @@ describe('<lion-calendar>', () => {
1269
1289
  expect(elObj.activeMonth).to.equal('December');
1270
1290
  expect(elObj.activeYear).to.equal('2000');
1271
1291
 
1272
- el.__contentWrapperElement?.dispatchEvent(
1273
- new KeyboardEvent('keydown', { key: 'ArrowRight' }),
1274
- );
1292
+ focusSelectedDate(el);
1293
+ await sendKeys({ press: 'ArrowRight' });
1275
1294
  await el.updateComplete;
1276
1295
  expect(elObj.activeMonth).to.equal('January');
1277
1296
  expect(elObj.activeYear).to.equal('2001');
@@ -1286,9 +1305,8 @@ describe('<lion-calendar>', () => {
1286
1305
  expect(elObj.activeMonth).to.equal('January');
1287
1306
  expect(elObj.activeYear).to.equal('2001');
1288
1307
 
1289
- el.__contentWrapperElement?.dispatchEvent(
1290
- new KeyboardEvent('keydown', { key: 'ArrowLeft' }),
1291
- );
1308
+ focusSelectedDate(el);
1309
+ await sendKeys({ press: 'ArrowLeft' });
1292
1310
  await el.updateComplete;
1293
1311
  expect(elObj.activeMonth).to.equal('December');
1294
1312
  expect(elObj.activeYear).to.equal('2000');
@@ -1303,9 +1321,8 @@ describe('<lion-calendar>', () => {
1303
1321
  expect(elObj.activeMonth).to.equal('December');
1304
1322
  expect(elObj.activeYear).to.equal('2000');
1305
1323
 
1306
- el.__contentWrapperElement?.dispatchEvent(
1307
- new KeyboardEvent('keydown', { key: 'ArrowDown' }),
1308
- );
1324
+ focusSelectedDate(el);
1325
+ await sendKeys({ press: 'ArrowDown' });
1309
1326
  await el.updateComplete;
1310
1327
  expect(elObj.activeMonth).to.equal('January');
1311
1328
  expect(elObj.activeYear).to.equal('2001');
@@ -1320,9 +1337,8 @@ describe('<lion-calendar>', () => {
1320
1337
  expect(elObj.activeMonth).to.equal('January');
1321
1338
  expect(elObj.activeYear).to.equal('2001');
1322
1339
 
1323
- el.__contentWrapperElement?.dispatchEvent(
1324
- new KeyboardEvent('keydown', { key: 'ArrowUp' }),
1325
- );
1340
+ focusSelectedDate(el);
1341
+ await sendKeys({ press: 'ArrowUp' });
1326
1342
  await el.updateComplete;
1327
1343
  expect(elObj.activeMonth).to.equal('December');
1328
1344
  expect(elObj.activeYear).to.equal('2000');
@@ -1659,4 +1675,8 @@ describe('<lion-calendar>', () => {
1659
1675
  expect(el).not.to.be.displayed;
1660
1676
  });
1661
1677
  });
1678
+
1679
+ describe('Run suite', () => {
1680
+ runCalendarSuite();
1681
+ });
1662
1682
  });
@@ -0,0 +1,79 @@
1
+ import { LionCalendar } from '@lion/ui/calendar.js';
2
+ import { expect, html, fixture, waitUntil, unsafeStatic, defineCE } from '@open-wc/testing';
3
+ import sinon from 'sinon';
4
+ import '@lion/ui/define/lion-calendar.js';
5
+ import { sendKeys } from '@web/test-runner-commands';
6
+
7
+ export function runCalendarSuite({ klass = LionCalendar } = {}) {
8
+ const tagStringCalendar = defineCE(class extends klass {});
9
+ const tagCalendar = unsafeStatic(tagStringCalendar);
10
+
11
+ it('should fire keyboard press events on keyup and not on keydown', async () => {
12
+ /**
13
+ * This test covers a corner case where `keydown`, `keyup`, and `focus` events happen across different elements.
14
+ *
15
+ * Setup:
16
+ * - There is a dialog invoker button that has a keyup handler.
17
+ * - The dialog invoker button gets focused (important!) and then opens the dialog on click.
18
+ * - The dialog contains lion-calendar.
19
+ * - When a date is selected, the dialog closes.
20
+ *
21
+ * Expected behavior:
22
+ * - The dialog invoker button's `keyup` handler should not run when a date is selected in the calendar.
23
+ *
24
+ * The problem:
25
+ * - Pressing Enter on a date is handled by `keydown` inside `lion-calendar`.
26
+ * - The dialog then closes on `user-selected-date-changed` event.
27
+ * - The browser automatically returns focus to the previously focused element (the dialog invoker button).
28
+ * - The keyup event then fires on the now-focused dialog invoker button, triggering its keyup
29
+ * handler unexpectedly.
30
+ *
31
+ * Fix being verified:
32
+ * - Fire `user-selected-date-changed` on `keyup` instead of `keydown`.
33
+ * - This prevents the dialog invoker button from receiving and handling that `keyup` after the dialog closes.
34
+ */
35
+
36
+ /** @type {HTMLElement} */
37
+ let el;
38
+ const keyUpSpy = sinon.spy();
39
+ const getDialogEl = () => el.querySelector('dialog');
40
+ const openDialogButtonClickHandler = () => getDialogEl()?.showModal();
41
+ const userSelectedDateChangedHandler = () => getDialogEl()?.close();
42
+
43
+ el = await fixture(html`
44
+ <div>
45
+ <button
46
+ @keyup="${keyUpSpy}"
47
+ @click="${openDialogButtonClickHandler}"
48
+ >
49
+ Open Dialog
50
+ </button>
51
+ <dialog>
52
+ <${tagCalendar}
53
+ .selectedDate="${new Date('2000/10/12')}"
54
+ @user-selected-date-changed="${userSelectedDateChangedHandler}"
55
+ >
56
+ </${tagCalendar}>
57
+ </dialog>
58
+ </div>
59
+ `);
60
+
61
+ const openDialogButton = el.querySelector('button');
62
+ const calendarEl = /** @type {LionCalendar} */ (el.querySelector(tagStringCalendar));
63
+ await calendarEl?.updateComplete;
64
+
65
+ openDialogButton?.focus();
66
+ openDialogButton?.click();
67
+ /**
68
+ * @returns {HTMLElement | null | undefined}
69
+ */
70
+ const getSelectedDateEl = () =>
71
+ calendarEl?.shadowRoot?.querySelector('.calendar__day-button[selected]');
72
+ await waitUntil(getSelectedDateEl);
73
+ getSelectedDateEl()?.focus();
74
+ await sendKeys({ press: 'ArrowLeft' });
75
+ await sendKeys({ press: 'Enter' });
76
+
77
+ expect(keyUpSpy.called).to.equal(false);
78
+ });
79
+ }
@@ -238,7 +238,7 @@ const ChoiceInputMixinImplementation = superclass =>
238
238
  */
239
239
  // eslint-disable-next-line no-unused-vars
240
240
  _toggleChecked(ev) {
241
- if (this.disabled) {
241
+ if (this.disabled || this.readOnly) {
242
242
  return;
243
243
  }
244
244
  this._isHandlingUserInput = true;
@@ -398,14 +398,16 @@ export class LionInputAmountDropdown extends LionInputAmount {
398
398
 
399
399
  /**
400
400
  * Used for Required validation and computation of interaction states.
401
- * We need to override this, because we prefill the input with the currency code, but for proper UX,
402
- * we don't consider this as having interaction state `prefilled`
403
- * @param {string} modelValue
401
+ * We only consider the amount part for emptiness checks.
402
+ * Missing, null, empty-string or NaN amount is considered empty.
404
403
  * @return {boolean}
405
404
  * @protected
406
405
  */
407
406
  _isEmpty(modelValue = this.modelValue) {
408
- return super._isEmpty(modelValue) || this.currency === this.__initializedCurrencyCode;
407
+ const isNumberValue =
408
+ typeof modelValue?.amount === 'number' &&
409
+ (modelValue?.amount === 0 || Number.isNaN(modelValue?.amount));
410
+ return !modelValue?.amount && !isNumberValue;
409
411
  }
410
412
 
411
413
  /**
@@ -1,6 +1,7 @@
1
1
  import { mimicUserChangingDropdown } from '@lion/ui/input-amount-dropdown-test-helpers.js';
2
2
  import { runInputAmountDropdownSuite } from '@lion/ui/input-amount-dropdown-test-suites.js';
3
3
  import { LionInputAmountDropdown } from '@lion/ui/input-amount-dropdown.js';
4
+ import { Required } from '@lion/ui/form-core.js';
4
5
  import { aTimeout, expect, fixture } from '@open-wc/testing';
5
6
  import { LionSelectRich } from '@lion/ui/select-rich.js';
6
7
  import { repeat } from 'lit/directives/repeat.js';
@@ -194,7 +195,25 @@ describe('WithFormControlInputAmountDropdown', () => {
194
195
  el.modelValue = { currency: 'EUR' };
195
196
  await aTimeout(0);
196
197
  el.modelValue = { currency: 'EUR', amount: '' };
197
- expect(el.hasFeedbackFor).to.deep.equal(['error']);
198
+ expect(el.hasFeedbackFor).to.deep.equal([]);
198
199
  });
199
200
  });
201
+
202
+ it('includes error in showsFeedbackFor when required, empty, and touched & dirty', async () => {
203
+ const el = /** @type {LionInputAmountDropdown} */ (
204
+ await fixture(html`
205
+ <lion-input-amount-dropdown
206
+ .allowedCurrencies="${['GBP', 'EUR']}"
207
+ .validators="${[new Required()]}"
208
+ ></lion-input-amount-dropdown>
209
+ `)
210
+ );
211
+ await el.updateComplete;
212
+ el.touched = true;
213
+ el.dirty = true;
214
+ await el.updateComplete;
215
+ await el.feedbackComplete;
216
+ expect(el.hasFeedbackFor).to.include('error', 'hasFeedbackFor');
217
+ expect(el.showsFeedbackFor).to.include('error', 'showsFeedbackFor');
218
+ });
200
219
  });
@@ -317,5 +317,29 @@ export function runInputAmountDropdownSuite({ klass } = { klass: LionInputAmount
317
317
  // @ts-ignore
318
318
  expect(el._isEmpty(), 'empty').to.be.true;
319
319
  });
320
+
321
+ it('only checks modelValue.amount', async () => {
322
+ const el = await fixture(html` <${tag}></${tag}> `);
323
+
324
+ el.modelValue = { currency: 'GBP', amount: 1 };
325
+ await el.updateComplete;
326
+ // @ts-ignore
327
+ expect(el._isEmpty(), 'filled amount').to.be.false;
328
+
329
+ el.modelValue = { currency: 'EUR', amount: '' };
330
+ await el.updateComplete;
331
+ // @ts-ignore
332
+ expect(el._isEmpty(), 'empty amount').to.be.true;
333
+
334
+ el.modelValue = { currency: 'EUR' };
335
+ await el.updateComplete;
336
+ // @ts-ignore
337
+ expect(el._isEmpty(), 'missing amount property').to.be.true;
338
+
339
+ el.modelValue = null;
340
+ await el.updateComplete;
341
+ // @ts-ignore
342
+ expect(el._isEmpty(), 'null modelValue').to.be.true;
343
+ });
320
344
  });
321
345
  }
@@ -4,11 +4,12 @@ import { IsDateDisabled, MaxDate, MinDate, MinMaxDate } from '@lion/ui/form-core
4
4
  import { aTimeout, defineCE, expect, fixture as _fixture, nextFrame } from '@open-wc/testing';
5
5
  import { mimicClick } from '@lion/ui/overlays-test-helpers.js';
6
6
  import sinon from 'sinon';
7
- import { setViewport } from '@web/test-runner-commands';
7
+ import { sendKeys, setViewport } from '@web/test-runner-commands';
8
8
  import { DatepickerInputObject } from '@lion/ui/input-datepicker-test-helpers.js';
9
9
  import { LionInputDatepicker } from '@lion/ui/input-datepicker.js';
10
10
 
11
11
  import '@lion/ui/define/lion-input-datepicker.js';
12
+ import { runDatepickerSuite } from '../test-suites/LionInputDatepicker.suite.js';
12
13
 
13
14
  /**
14
15
  * @typedef {import('lit').TemplateResult} TemplateResult
@@ -479,10 +480,14 @@ describe('<lion-input-datepicker>', () => {
479
480
  // Open the calendar
480
481
  await elObj.openCalendar();
481
482
 
483
+ const selectedDateEl = calendarEl?.shadowRoot?.querySelector(
484
+ '.calendar__day-button[selected]',
485
+ );
486
+ // @ts-ignore
487
+ selectedDateEl?.focus();
488
+
482
489
  // Move focus to 18th of December
483
- calendarEl.shadowRoot
484
- ?.querySelector('#js-content-wrapper')
485
- ?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }));
490
+ await sendKeys({ press: 'ArrowRight' });
486
491
 
487
492
  expect(/** @type {Date} */ (calendarEl.focusedDate).getTime()).to.equal(
488
493
  new Date('December 18, 2020 03:24:00 GMT+0000').getTime(),
@@ -834,4 +839,8 @@ describe('<lion-input-datepicker>', () => {
834
839
  ).to.be.true;
835
840
  });
836
841
  });
842
+
843
+ describe('Run suite', () => {
844
+ runDatepickerSuite();
845
+ });
837
846
  });
@@ -0,0 +1,35 @@
1
+ import { expect, html, fixture, waitUntil, unsafeStatic } from '@open-wc/testing';
2
+ import { sendKeys } from '@web/test-runner-commands';
3
+
4
+ /** @typedef {import('@lion/ui/input-datepicker.js').LionInputDatepicker} LionInputDatepicker */
5
+
6
+ export function runDatepickerSuite({
7
+ datepickerTagName = 'lion-input-datepicker',
8
+ calendarTagName = 'lion-calendar',
9
+ } = {}) {
10
+ const tag = unsafeStatic(datepickerTagName);
11
+
12
+ it('should not reopen the datepicker popup when the date is selected by keyboard', async () => {
13
+ /** @type {LionInputDatepicker} */
14
+ const el = await fixture(html`<${tag} .modelValue="${new Date('2018/05/30')}"></${tag}>`);
15
+ await el.updateComplete;
16
+
17
+ // @ts-ignore
18
+ const openDatepickerButton = el._invokerNode;
19
+ openDatepickerButton?.focus();
20
+ openDatepickerButton?.click();
21
+ /**
22
+ * @returns {HTMLElement | null | undefined}
23
+ */
24
+ const getSelectedDateEl = () =>
25
+ el?.shadowRoot
26
+ ?.querySelector(calendarTagName)
27
+ ?.shadowRoot?.querySelector('.calendar__day-button[selected]');
28
+ await waitUntil(getSelectedDateEl);
29
+ getSelectedDateEl()?.focus();
30
+ await sendKeys({ press: 'ArrowLeft' });
31
+ await sendKeys({ press: 'Enter' });
32
+
33
+ expect(el.opened).to.be.false;
34
+ });
35
+ }
@@ -10,6 +10,8 @@ import { LocalizeMixin } from '@lion/ui/localize-no-side-effects.js';
10
10
  export class LionInputEmail extends LocalizeMixin(LionInput) {
11
11
  constructor() {
12
12
  super();
13
+ this.autocomplete = 'email';
14
+
13
15
  // local-part@domain where the local part may be up to 64 characters long
14
16
  // and the domain may have a maximum of 255 characters.
15
17
  // @see https://www.wikiwand.com/en/Email_address
@@ -15,6 +15,12 @@ describe('<lion-input-email>', () => {
15
15
  expect(_inputNode.type).to.equal('text');
16
16
  });
17
17
 
18
+ it('sets autocomplete to "email"', async () => {
19
+ const el = await fixture(`<lion-input-email></lion-input-email>`);
20
+ // @ts-expect-error [allow-protected] inside tests
21
+ expect(el._inputNode.getAttribute('autocomplete')).to.equal('email');
22
+ });
23
+
18
24
  it('has validator "IsEmail" applied by default', async () => {
19
25
  // More elaborate tests can be found in lion-validate/test/StringValidators.test.js
20
26
  const el = await fixture(`<lion-input-email></lion-input-email>`);
@@ -2,13 +2,14 @@ import { Validator } from '@lion/ui/form-core.js';
2
2
  import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
3
3
 
4
4
  /**
5
+ * @typedef {import('../../form-core/types/validate/index.js').FeedbackMessageData} FeedbackMessageData
5
6
  * @typedef {import('../../form-core/types/validate/validate.js').ValidatorConfig} ValidatorConfig
6
7
  * @typedef {import('../../form-core/types/validate/validate.js').ValidatorParam} ValidatorParam
7
8
  * @typedef {import('../../form-core/types/validate/validate.js').ValidatorOutcome} ValidatorOutcome
8
9
  * @typedef {import('../types/input-file.js').InputFile} InputFile
9
10
  */
10
11
 
11
- /* eslint max-classes-per-file: ["error", 2] */
12
+ /* eslint max-classes-per-file: ["error", 3] */
12
13
  export class IsAcceptedFile extends Validator {
13
14
  static validatorName = 'IsAcceptedFile';
14
15
 
@@ -112,3 +113,32 @@ export class DuplicateFileNames extends Validator {
112
113
  return localizeManager.msg('lion-input-file:uploadTextDuplicateFileName');
113
114
  }
114
115
  }
116
+
117
+ export class MaxFileCount extends Validator {
118
+ static get validatorName() {
119
+ return 'MaxFileCount';
120
+ }
121
+
122
+ /**
123
+ * @param {Partial<FeedbackMessageData>} [data]
124
+ * @returns {Promise<string|Element>}
125
+ */
126
+ static async getMessage(data) {
127
+ const localizeManager = getLocalizeManager();
128
+ if (!data?.params || typeof data.params !== 'number' || data.params <= 0) {
129
+ return 'Invalid MaxFileCount parameter. Please provide a valid number greater than 0.';
130
+ }
131
+ return localizeManager.msg('lion-input-file:maxFileCountExceeded', {
132
+ maxFileCount: data.params,
133
+ });
134
+ }
135
+
136
+ /**
137
+ * @param {File[]} value
138
+ * @param {number} maxFiles
139
+ * @returns {boolean}
140
+ */
141
+ execute(value, maxFiles = this.param || 0) {
142
+ return maxFiles > 0 && value.length > maxFiles;
143
+ }
144
+ }
@@ -1,4 +1,5 @@
1
1
  import { expect } from '@open-wc/testing';
2
+ import { MaxFileCount } from '@lion/ui/input-file.js';
2
3
  import { IsAcceptedFile } from '../src/validators.js';
3
4
 
4
5
  describe('lion-input-file: IsAcceptedFile', () => {
@@ -167,4 +168,34 @@ describe('lion-input-file: IsAcceptedFile', () => {
167
168
  expect(returnVal).to.be.true;
168
169
  });
169
170
  });
171
+
172
+ describe('Max File Count', () => {
173
+ /**
174
+ *
175
+ * @param {string} filename
176
+ * @returns {File}
177
+ */
178
+ const newFile = filename => new File([], filename);
179
+
180
+ it('Allow up to 2 files', () => {
181
+ const validator = new MaxFileCount(2);
182
+ expect(MaxFileCount.validatorName).to.equal('MaxFileCount');
183
+
184
+ let isEnabled = validator.execute([newFile('file1.txt'), newFile('file2.txt')]);
185
+ expect(isEnabled).to.be.false;
186
+
187
+ isEnabled = validator.execute([
188
+ newFile('file1.txt'),
189
+ newFile('file2.txt'),
190
+ newFile('file3.txt'),
191
+ ]);
192
+ expect(isEnabled).to.be.true;
193
+ });
194
+
195
+ it('Allow any number of files with empty param', () => {
196
+ const validator = new MaxFileCount();
197
+ const isEnabled = validator.execute([newFile('file1.txt'), newFile('file2.txt')]);
198
+ expect(isEnabled).to.be.false;
199
+ });
200
+ });
170
201
  });
@@ -17,4 +17,5 @@ export default {
17
17
  selectTextDuplicateFileName: 'Файл със същото име на файл вече е налице.',
18
18
  selectTextMultipleFile: 'Избор на файлове',
19
19
  selectTextSingleFile: 'Избор на файл',
20
+ maxFileCountExceeded: 'Броят на качените файлове не може да надвишава {maxFileCount}.',
20
21
  };
@@ -18,4 +18,5 @@ export default {
18
18
  selectTextDuplicateFileName: 'Soubor se stejným názvem byl již přítomen.',
19
19
  selectTextMultipleFile: 'Vybrat soubory',
20
20
  selectTextSingleFile: 'Vybrat soubor',
21
+ maxFileCountExceeded: 'Počet nahraných souborů nesmí překročit {maxFileCount}.',
21
22
  };
@@ -17,4 +17,6 @@ export default {
17
17
  selectTextDuplicateFileName: 'Eine Datei mit demselben Dateinamen war bereits vorhanden.',
18
18
  selectTextMultipleFile: 'Dateien auswählen',
19
19
  selectTextSingleFile: 'Datei auswählen',
20
+ maxFileCountExceeded:
21
+ 'Die Anzahl der hochgeladenen Dateien darf {maxFileCount} nicht überschreiten.',
20
22
  };
@@ -16,4 +16,5 @@ export default {
16
16
  selectTextDuplicateFileName: 'A file with same filename was already present.',
17
17
  selectTextMultipleFile: 'Select files',
18
18
  selectTextSingleFile: 'Select file',
19
+ maxFileCountExceeded: 'The number of uploaded files must not exceed {maxFileCount}.',
19
20
  };
@@ -17,4 +17,5 @@ export default {
17
17
  selectTextDuplicateFileName: 'Ya había un archivo con el mismo nombre de archivo.',
18
18
  selectTextMultipleFile: 'Seleccionar archivos',
19
19
  selectTextSingleFile: 'Seleccionar archivo',
20
+ maxFileCountExceeded: 'El número de archivos cargados no puede superar {maxFileCount}.',
20
21
  };