@lion/ui 0.20.0 → 0.21.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 (108) hide show
  1. package/README.md +1 -1
  2. package/components/form-core/src/FormatMixin.js +10 -5
  3. package/components/form-core/test-helpers/mimicUserInput.js +1 -0
  4. package/components/form-core/test-suites/FormatMixin.suite.js +31 -14
  5. package/components/input-amount-dropdown/src/LionInputAmountDropdown.js +51 -8
  6. package/components/input-amount-dropdown/test/lion-input-amount-dropdown-integrations.test.js +5 -0
  7. package/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.js +1 -0
  8. package/components/input-datepicker/src/LionInputDatepicker.js +4 -0
  9. package/components/input-datepicker/test/lion-input-datepicker.test.js +55 -0
  10. package/components/input-range/src/LionInputRange.js +51 -8
  11. package/components/input-range/test/lion-input-range.test.js +114 -0
  12. package/components/input-tel/src/LionInputTel.js +1 -4
  13. package/components/input-tel-dropdown/test/LionInputTelDropdown.test.js +5 -1
  14. package/components/input-tel-dropdown/test-suites/LionInputTelDropdown--parentheses.suite.js +90 -0
  15. package/components/input-tel-dropdown/test-suites/types.ts +6 -0
  16. package/custom-elements.json +425 -66
  17. package/dist-types/components/form-core/src/FormatMixin.d.ts.map +1 -1
  18. package/dist-types/components/form-core/test-helpers/mimicUserInput.d.ts.map +1 -1
  19. package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts +8 -0
  20. package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts.map +1 -1
  21. package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts +24 -6
  22. package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts.map +1 -1
  23. package/dist-types/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.d.ts.map +1 -1
  24. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -0
  25. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts.map +1 -1
  26. package/dist-types/components/input-datepicker/test/lion-input-datepicker.test.d.ts.map +1 -1
  27. package/dist-types/components/input-range/src/LionInputRange.d.ts +11 -0
  28. package/dist-types/components/input-range/src/LionInputRange.d.ts.map +1 -1
  29. package/dist-types/components/input-tel/src/LionInputTel.d.ts.map +1 -1
  30. package/dist-types/components/input-tel-dropdown/test/LionInputTelDropdown.test.d.ts.map +1 -1
  31. package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown--parentheses.suite.d.ts +11 -0
  32. package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown--parentheses.suite.d.ts.map +1 -0
  33. package/dist-types/components/input-tel-dropdown/test-suites/types.d.ts +6 -0
  34. package/dist-types/components/input-tel-dropdown/test-suites/types.d.ts.map +1 -0
  35. package/dist-types/exports/input-tel-dropdown-test-suites.d.ts +1 -0
  36. package/dist-types/tsconfig.tsbuildinfo +1 -1
  37. package/docs/components/checkbox-group/api-table.md +4 -2
  38. package/docs/components/checkbox-group/overview.md +2 -2
  39. package/docs/components/collapsible/overview.md +1 -1
  40. package/docs/components/combobox/overview.md +1 -1
  41. package/docs/components/combobox/use-cases.md +1 -1
  42. package/docs/components/dialog/overview.md +1 -1
  43. package/docs/components/fieldset/overview.md +5 -5
  44. package/docs/components/form/overview.md +3 -3
  45. package/docs/components/form/use-cases.md +2 -2
  46. package/docs/components/icon/use-cases.md +1 -1
  47. package/docs/components/input/api-table.md +2 -1
  48. package/docs/components/input/overview.md +1 -1
  49. package/docs/components/input-amount/api-table.md +2 -1
  50. package/docs/components/input-amount/overview.md +3 -3
  51. package/docs/components/input-amount-dropdown/api-table.md +4 -3
  52. package/docs/components/input-date/api-table.md +2 -1
  53. package/docs/components/input-date/overview.md +3 -3
  54. package/docs/components/input-datepicker/api-table.md +2 -1
  55. package/docs/components/input-datepicker/overview.md +5 -5
  56. package/docs/components/input-email/api-table.md +2 -1
  57. package/docs/components/input-email/overview.md +2 -2
  58. package/docs/components/input-file/api-table.md +2 -1
  59. package/docs/components/input-file/overview.md +1 -1
  60. package/docs/components/input-iban/api-table.md +2 -1
  61. package/docs/components/input-iban/overview.md +2 -2
  62. package/docs/components/input-range/api-table.md +7 -1
  63. package/docs/components/input-range/overview.md +2 -2
  64. package/docs/components/input-range/use-cases.md +14 -0
  65. package/docs/components/input-stepper/api-table.md +2 -1
  66. package/docs/components/input-stepper/overview.md +1 -1
  67. package/docs/components/input-tel/api-table.md +2 -1
  68. package/docs/components/input-tel/overview.md +1 -1
  69. package/docs/components/input-tel/use-cases.md +6 -6
  70. package/docs/components/input-tel-dropdown/api-table.md +2 -1
  71. package/docs/components/input-tel-dropdown/overview.md +1 -1
  72. package/docs/components/listbox/api-table.md +2 -1
  73. package/docs/components/listbox/use-cases.md +1 -1
  74. package/docs/components/radio-group/api-table.md +2 -1
  75. package/docs/components/radio-group/overview.md +3 -3
  76. package/docs/components/select/api-table.md +2 -1
  77. package/docs/components/select/use-cases.md +1 -1
  78. package/docs/components/select-rich/use-cases.md +2 -2
  79. package/docs/components/switch/api-table.md +2 -1
  80. package/docs/components/tabs/examples.md +1 -1
  81. package/docs/components/textarea/api-table.md +2 -1
  82. package/docs/fundamentals/index.md +4 -4
  83. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  84. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  85. package/docs/fundamentals/rationales/accessibility.md +1 -1
  86. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  87. package/docs/fundamentals/systems/core/overview.md +1 -1
  88. package/docs/fundamentals/systems/form/api-table.md +2 -1
  89. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  90. package/docs/fundamentals/systems/form/model-value.md +1 -1
  91. package/docs/fundamentals/systems/form/overview.md +28 -28
  92. package/docs/fundamentals/systems/form/validate.md +2 -2
  93. package/docs/fundamentals/systems/localize/overview.md +4 -4
  94. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  95. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  96. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  97. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  98. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  99. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  100. package/docs/guides/how-to/create-a-form.md +4 -4
  101. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  102. package/docs/guides/how-to/index.md +1 -1
  103. package/docs/guides/index.md +8 -8
  104. package/docs/guides/principles/index.md +5 -5
  105. package/docs/guides/principles/styling.md +1 -1
  106. package/exports/input-tel-dropdown-test-suites.js +1 -0
  107. package/input-tel-dropdown-test-suites.js +1 -0
  108. package/package.json +1 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@
37
37
  <a href="https://lion.js.org/blog/">Blog</a>
38
38
  </p>
39
39
 
40
- **Lion is a set of highly performant, accessible and flexible Web Components.!**
40
+ **Lion is a set of highly performant, accessible and flexible Web Components!**
41
41
 
42
42
  They provide an unopinionated, white-label layer that can be extended to your own layer of components.
43
43
 
@@ -73,7 +73,7 @@ const FormatMixinImplementation = superclass =>
73
73
  /**
74
74
  * During format/parse, we sometimes need to know how the current viewValue the user
75
75
  * interacts with "came to be". Was it already formatted before for instance?
76
- * If so, in case of an amount input, we might want to stick to the curent interpretation of separators.
76
+ * If so, in case of an amount input, we might want to stick to the current interpretation of separators.
77
77
  */
78
78
  #viewValueState = {
79
79
  didFormatterOutputSyncToView: false,
@@ -124,13 +124,19 @@ const FormatMixinImplementation = superclass =>
124
124
  }
125
125
  }
126
126
 
127
+ get _isEditingTextField() {
128
+ // For backward compatibility. Ideally we check whether the input is focused,
129
+ // like we do in "hybrid" (picker + textbox) components like datepicker
130
+ return this._isHandlingUserInput;
131
+ }
132
+
127
133
  /**
128
134
  * Preprocessors could be considered 'live formatters'. Their result is shown to the user
129
135
  * on keyup instead of after blurring the field. The biggest difference between preprocessors
130
136
  * and formatters is their moment of execution: preprocessors are run before modelValue is
131
137
  * computed (and work based on view value), whereas formatters are run after the parser (and
132
138
  * are based on modelValue)
133
- * Automatically formats code while typing. It depends on a preprocessro that smartly
139
+ * Automatically formats code while typing. It depends on a preprocessor that smartly
134
140
  * updates the viewValue and caret position for best UX.
135
141
  * @example
136
142
  * ```js
@@ -283,15 +289,14 @@ const FormatMixinImplementation = superclass =>
283
289
  // - Why check for this.hasError?
284
290
  // We only want to format values that are considered valid. For best UX,
285
291
  // we only 'reward' valid inputs.
286
- // - Why check for _isHandlingUserInput?
292
+ // - Why check for _isEditingTextField?
287
293
  // Downwards sync is prevented whenever we are in an `@user-input-changed` flow, [2].
288
294
  // If we are in a 'imperatively set `.modelValue`' flow, [1], we want to reflect back
289
295
  // the value, no matter what.
290
296
  // This means, whenever we are in hasError and modelValue is set
291
297
  // imperatively, we DO want to format a value (it is the only way to get meaningful
292
298
  // input into `._inputNode` with modelValue as input)
293
-
294
- if (this._isHandlingUserInput && this.hasFeedbackFor?.includes('error') && this._inputNode) {
299
+ if (this._isEditingTextField && this.hasFeedbackFor?.includes('error') && this._inputNode) {
295
300
  return this.value;
296
301
  }
297
302
 
@@ -12,5 +12,6 @@ export function mimicUserInput(formControl, newViewValue, { caretIndex } = {}) {
12
12
  // eslint-disable-next-line no-param-reassign
13
13
  formControl._inputNode.selectionEnd = caretIndex;
14
14
  }
15
+ formControl._inputNode.focus();
15
16
  formControl._inputNode.dispatchEvent(new Event('input', { bubbles: true }));
16
17
  }
@@ -44,6 +44,8 @@ class FormatClass extends FormatMixin(LitElement) {
44
44
  * @property {(el: FormatClass) => any} [getExpectedInitialModelValue] - Optional. Expected initial modelValue state when element is created without a value. If not provided, defaults will be generated based on modelValueType.
45
45
  * @property {(el: FormatClass) => any} [getExpectedInitialFormattedValue] - Optional. Expected initial formattedValue state when element is created without a value. If not provided, defaults will be generated based on modelValueType.
46
46
  * @property {(el: FormatClass) => any} [getExpectedInitialSerializedValue] - Optional. Expected initial serializedValue state when element is created without a value. If not provided, defaults will be generated based on modelValueType.
47
+ * @property {(el: FormatClass) => any} [getExpectedModelValueForEmptyInputNode] - Optional. Expected modelValue after user clears the input node value. Defaults to empty string.
48
+ * @property {(value: string, opts?: any) => any} [parser] - Optional. Parser override used by tests. When provided, it is applied before connectedCallback so fallback init sync uses it.
47
49
  * @property {number} [valueChangeCounterOffset] - Optional. Offset for model-value-changed event counter. Use 0 for most types, 1 for Date types where parseDate() creates new objects. If not provided, defaults will be generated based on modelValueType.
48
50
  */
49
51
 
@@ -180,6 +182,10 @@ export function runFormatMixinSuite(customConfig) {
180
182
  const getExpectedInitialSerializedValue =
181
183
  cfg.getExpectedInitialSerializedValue ||
182
184
  createDefaultGetExpectedInitialSerializedValue(cfg.modelValueType);
185
+ const getExpectedModelValueForEmptyInputNode =
186
+ cfg.getExpectedModelValueForEmptyInputNode || (() => '');
187
+ const defaultParser = (/** @type {string} */ value) => value.replace('foo: ', '');
188
+ const parser = cfg.parser || defaultParser;
183
189
  const valueChangeCounterOffset =
184
190
  cfg.valueChangeCounterOffset !== undefined
185
191
  ? cfg.valueChangeCounterOffset
@@ -225,6 +231,13 @@ export function runFormatMixinSuite(customConfig) {
225
231
  return getExpectedInitialSerializedValue(el);
226
232
  }
227
233
 
234
+ /**
235
+ * @param {FormatClass} el
236
+ */
237
+ function generateExpectedModelValueForEmptyInputNode(el) {
238
+ return getExpectedModelValueForEmptyInputNode(el);
239
+ }
240
+
228
241
  /**
229
242
  * @param {any} actual
230
243
  * @param {any} expected
@@ -253,7 +266,7 @@ export function runFormatMixinSuite(customConfig) {
253
266
  fooFormat = await fixture(html`
254
267
  <${tag}
255
268
  .formatter="${/** @param {string} value */ value => `foo: ${value}`}"
256
- .parser="${/** @param {string} value */ value => value.replace('foo: ', '')}"
269
+ .parser="${parser}"
257
270
  .serializer="${/** @param {string} value */ value => `[foo] ${value}`}"
258
271
  .deserializer="${/** @param {string} value */ value => value.replace('[foo] ', '')}"
259
272
  >
@@ -368,7 +381,7 @@ export function runFormatMixinSuite(customConfig) {
368
381
  <${tag}
369
382
  value="string"
370
383
  .formatter=${/** @param {string} value */ value => `foo: ${value}`}
371
- .parser=${/** @param {string} value */ value => value.replace('foo: ', '')}
384
+ .parser=${parser}
372
385
  .serializer=${/** @param {string} value */ value => `[foo] ${value}`}
373
386
  .deserializer=${/** @param {string} value */ value => value.replace('[foo] ', '')}
374
387
  >
@@ -378,12 +391,14 @@ export function runFormatMixinSuite(customConfig) {
378
391
  );
379
392
  // Now check if the format/parse/serialize loop has been triggered
380
393
  await formatElem.updateComplete;
381
- expect(formatElem.formattedValue).to.equal('foo: string');
394
+ const expectedFormattedValue = `foo: ${formatElem.modelValue}`;
395
+ const expectedSerializedValue = `[foo] ${formatElem.modelValue}`;
396
+
397
+ expect(formatElem.formattedValue).to.equal(expectedFormattedValue);
382
398
 
383
- expect(formatElem._inputNode.value).to.equal('foo: string');
399
+ expect(formatElem._inputNode.value).to.equal(expectedFormattedValue);
384
400
 
385
- expect(formatElem.serializedValue).to.equal('[foo] string');
386
- expect(formatElem.modelValue).to.equal('string');
401
+ expect(formatElem.serializedValue).to.equal(expectedSerializedValue);
387
402
  });
388
403
 
389
404
  describe('Unparseable values', () => {
@@ -445,9 +460,11 @@ export function runFormatMixinSuite(customConfig) {
445
460
  );
446
461
  // This could happen when the user erases the input value
447
462
  mimicUserInput(el, '');
448
- // For backwards compatibility, we keep the modelValue an empty string here.
449
- // Undefined would be more appropriate 'conceptually', however
450
- expect(el.modelValue).to.equal('');
463
+ expectValue(
464
+ el.modelValue,
465
+ generateExpectedModelValueForEmptyInputNode(el),
466
+ 'modelValue after clearing input node value',
467
+ );
451
468
  });
452
469
  });
453
470
  });
@@ -794,13 +811,13 @@ export function runFormatMixinSuite(customConfig) {
794
811
  `)
795
812
  );
796
813
 
797
- expect(parserSpy.callCount).to.equal(1);
814
+ const initialParserCallCount = parserSpy.callCount;
798
815
  // This could happen for instance in a reset
799
816
  el.modelValue = undefined;
800
- expect(parserSpy.callCount).to.equal(1);
817
+ expect(parserSpy.callCount).to.equal(initialParserCallCount);
801
818
  // This could happen when the user erases the input value
802
819
  mimicUserInput(el, '');
803
- expect(parserSpy.callCount).to.equal(1);
820
+ expect(parserSpy.callCount).to.equal(initialParserCallCount);
804
821
  });
805
822
  });
806
823
 
@@ -824,12 +841,12 @@ export function runFormatMixinSuite(customConfig) {
824
841
 
825
842
  const { _inputNode } = getFormControlMembers(el);
826
843
 
827
- expect(preprocessorSpy.callCount).to.equal(1);
844
+ const initialPreprocessorCallCount = preprocessorSpy.callCount;
828
845
 
829
846
  const parserSpy = sinon.spy(el, 'parser');
830
847
  mimicUserInput(el, toBeCorrectedVal);
831
848
 
832
- expect(preprocessorSpy.callCount).to.equal(2);
849
+ expect(preprocessorSpy.callCount).to.equal(initialPreprocessorCallCount + 1);
833
850
  expect(parserSpy.lastCall.args[0]).to.equal(val);
834
851
  expect(_inputNode.value).to.equal(val);
835
852
  });
@@ -73,12 +73,48 @@ export class LionInputAmountDropdown extends LionInputAmount {
73
73
  * @configure LitElement
74
74
  * @type {any}
75
75
  */
76
- static properties = {
77
- modelValue: { type: Object, hasChanged: hasChangedAmountDropdownModelValue },
78
- preferredCurrencies: { type: Array },
79
- allowedCurrencies: { type: Array },
80
- __dropdownSlot: { type: String },
81
- };
76
+ static get properties() {
77
+ return {
78
+ modelValue: {
79
+ type: Object,
80
+ attribute: false,
81
+ hasChanged: hasChangedAmountDropdownModelValue,
82
+ },
83
+ preferredCurrencies: { type: Array },
84
+ allowedCurrencies: { type: Array },
85
+ __dropdownSlot: { type: String },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * The internal modelValue is a combination of the amount and the currency code. The amount is a number, the currency code is a string.
91
+ * @private
92
+ * @type {import('../types/index.js').AmountDropdownModelValue}
93
+ */
94
+ __modelValue;
95
+
96
+ /**
97
+ * For a FormControl to function properly, its modelValue, serializedValue and viewValue/formattedValue
98
+ * should always translate in two directions. This is expected by `_callParser` method. It guards for empty
99
+ * viewValues. As the viewValue (`.value`) only concerns the amount part and not the currency part in
100
+ * InputAmountDropdown, this contract is broken.
101
+ * Below, we compensate for this.
102
+ */
103
+ // @ts-expect-error - modelValue is overridden as an accessor but is defined as a property in parent class
104
+ set modelValue(value) {
105
+ if (value) {
106
+ this.__modelValue = /** @type {import('../types/index.js').AmountDropdownModelValue} */ (
107
+ value
108
+ );
109
+ // Below is needed because A1 and A2 in _callParser (FormatMixin) return values that are the wrong type.
110
+ } else {
111
+ this.__modelValue = { currency: this.currency, amount: '' };
112
+ }
113
+ }
114
+
115
+ get modelValue() {
116
+ return /** @type {import('../types/index.js').AmountDropdownModelValue} */ (this.__modelValue);
117
+ }
82
118
 
83
119
  static localizeNamespaces = [
84
120
  { 'lion-input-amount-dropdown': localizeNamespaceLoader },
@@ -292,6 +328,9 @@ export class LionInputAmountDropdown extends LionInputAmount {
292
328
 
293
329
  this.parser = parseAmount;
294
330
 
331
+ /** @type {import('../types/index.js').AmountDropdownModelValue} */
332
+ this.__modelValue = { currency: this.currency, amount: '' };
333
+
295
334
  /**
296
335
  * @param {import("../types/index.js").AmountDropdownModelValue} modelValue
297
336
  * @param {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} [givenOptions] Locale Options
@@ -406,7 +445,8 @@ export class LionInputAmountDropdown extends LionInputAmount {
406
445
  * @protected
407
446
  */
408
447
  _initModelValueBasedOnDropdown() {
409
- if (!this._initialModelValue && !this.dirty) {
448
+ const { currency, amount } = this._initialModelValue || {};
449
+ if (!currency && !amount && !this.dirty) {
410
450
  this.__initializedCurrencyCode = this.currency;
411
451
  this._initialModelValue = { currency: this.currency };
412
452
  this.modelValue = this._initialModelValue;
@@ -533,7 +573,10 @@ export class LionInputAmountDropdown extends LionInputAmount {
533
573
  }
534
574
 
535
575
  // 2. Try to get the currency from user input
536
- if (this.modelValue?.currency && this.allowedCurrencies?.includes(this.modelValue?.currency)) {
576
+ if (
577
+ this.modelValue?.currency &&
578
+ this.allowedCurrencies?.includes(/** @type {CurrencyCode} */ (this.modelValue?.currency))
579
+ ) {
537
580
  this.currency = this.modelValue.currency;
538
581
  return;
539
582
  }
@@ -8,6 +8,7 @@ describe('<lion-input-amount-dropdown> integrations', () => {
8
8
  runFormatMixinSuite({
9
9
  tagString,
10
10
  modelValueType: Object,
11
+ parser: value => value.replace('foo: ', ''),
11
12
  valueToggler: ({ toggleValue, viewValue }) => {
12
13
  if (viewValue) {
13
14
  return !toggleValue ? '123' : '456';
@@ -17,6 +18,10 @@ describe('<lion-input-amount-dropdown> integrations', () => {
17
18
  getExpectedInitialModelValue: el => ({ currency: /** @type {any} */ (el).currency }),
18
19
  getExpectedInitialFormattedValue: el => ({ currency: /** @type {any} */ (el).currency }),
19
20
  getExpectedInitialSerializedValue: el => ({ currency: /** @type {any} */ (el).currency }),
21
+ getExpectedModelValueForEmptyInputNode: el => ({
22
+ currency: /** @type {any} */ (el).currency,
23
+ amount: '',
24
+ }),
20
25
  valueChangeCounterOffset: 0,
21
26
  });
22
27
  });
@@ -392,6 +392,7 @@ export function runInputAmountDropdownSuite({ klass } = { klass: LionInputAmount
392
392
  // @ts-ignore
393
393
  expect(el._isEmpty(), 'missing amount property').to.be.true;
394
394
 
395
+ // @ts-ignore
395
396
  el.modelValue = null;
396
397
  await el.updateComplete;
397
398
  // @ts-ignore
@@ -130,6 +130,10 @@ export class LionInputDatepicker extends ScopedElementsMixin(
130
130
  );
131
131
  }
132
132
 
133
+ get _isEditingTextField() {
134
+ return this.focused && this._isHandlingUserInput;
135
+ }
136
+
133
137
  constructor() {
134
138
  super();
135
139
  /** @private */
@@ -3,8 +3,10 @@ import { html, LitElement } from 'lit';
3
3
  import { IsDateDisabled, MaxDate, MinDate, MinMaxDate } from '@lion/ui/form-core.js';
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
+ import { parseDate } from '@lion/ui/localize.js';
6
7
  import sinon from 'sinon';
7
8
  import { sendKeys, setViewport } from '@web/test-runner-commands';
9
+ import { AlwaysInvalid } from '@lion/ui/form-core-test-helpers.js';
8
10
  import { DatepickerInputObject } from '@lion/ui/input-datepicker-test-helpers.js';
9
11
  import { LionInputDatepicker } from '@lion/ui/input-datepicker.js';
10
12
 
@@ -455,6 +457,59 @@ describe('<lion-input-datepicker>', () => {
455
457
  expect(el.validationStates.error).to.have.property('IsDate');
456
458
  });
457
459
 
460
+ it('syncs view value from calendar if custom validator is in error state', async () => {
461
+ const el = await fixture(html`
462
+ <lion-input-datepicker .validators=${[new AlwaysInvalid()]}></lion-input-datepicker>
463
+ `);
464
+
465
+ const elObj = new DatepickerInputObject(el);
466
+ await elObj.openCalendar();
467
+ await elObj.selectMonthDay(12);
468
+ await el.updateComplete; // safari take a little longer
469
+
470
+ expect(el.hasFeedbackFor).to.include('error');
471
+ expect(el.validationStates).to.have.property('error');
472
+ expect(el.validationStates.error).to.have.property('AlwaysInvalid');
473
+ expect(isSameDate(/** @type {Date} */ (parseDate(el.value)), elObj.calendarEl.selectedDate))
474
+ .to.be.true;
475
+ expect(
476
+ isSameDate(
477
+ // @ts-ignore [allow-protected] in test
478
+ /** @type {Date} */ (parseDate(el._inputNode.value)),
479
+ elObj.calendarEl.selectedDate,
480
+ ),
481
+ ).to.be.true;
482
+ });
483
+
484
+ it('syncs view value from calendar if custom validator is in error state, when there was a previous modelValue', async () => {
485
+ const modelValue = new Date('2022/12/30');
486
+ const el = await fixture(html`
487
+ <lion-input-datepicker
488
+ .modelValue="${modelValue}"
489
+ .validators=${[new AlwaysInvalid()]}
490
+ ></lion-input-datepicker>
491
+ `);
492
+ expect(isSameDate(/** @type {Date} */ (parseDate(el.value)), modelValue)).to.be.true;
493
+
494
+ const elObj = new DatepickerInputObject(el);
495
+ await elObj.openCalendar();
496
+ await elObj.selectMonthDay(12);
497
+ await el.updateComplete; // safari take a little longer
498
+
499
+ expect(el.hasFeedbackFor).to.include('error');
500
+ expect(el.validationStates).to.have.property('error');
501
+ expect(el.validationStates.error).to.have.property('AlwaysInvalid');
502
+ expect(isSameDate(/** @type {Date} */ (parseDate(el.value)), elObj.calendarEl.selectedDate))
503
+ .to.be.true;
504
+ expect(
505
+ isSameDate(
506
+ // @ts-ignore [allow-protected] in test
507
+ /** @type {Date} */ (parseDate(el._inputNode.value)),
508
+ elObj.calendarEl.selectedDate,
509
+ ),
510
+ ).to.be.true;
511
+ });
512
+
458
513
  /**
459
514
  * Not in scope:
460
515
  * - min/max attr (like platform has): could be added in future if observers needed
@@ -38,6 +38,14 @@ export class LionInputRange extends LocalizeMixin(LionInput) {
38
38
  type: Boolean,
39
39
  attribute: 'no-min-max-labels',
40
40
  },
41
+ minLabel: {
42
+ type: String,
43
+ attribute: 'min-label',
44
+ },
45
+ maxLabel: {
46
+ type: String,
47
+ attribute: 'max-label',
48
+ },
41
49
  };
42
50
  }
43
51
 
@@ -84,6 +92,25 @@ export class LionInputRange extends LocalizeMixin(LionInput) {
84
92
  return /** @type {HTMLInputElement} */ (super._inputNode);
85
93
  }
86
94
 
95
+ /**
96
+ * Get the display info for the current value
97
+ * @protected
98
+ * @returns {{ text: string, showUnit: boolean }}
99
+ */
100
+ get _valueDisplay() {
101
+ const currentValue = parseFloat(/** @type {string} */ (this.formattedValue));
102
+
103
+ if (this.minLabel && currentValue === this.min) {
104
+ return { text: this.minLabel, showUnit: false };
105
+ }
106
+
107
+ if (this.maxLabel && currentValue === this.max) {
108
+ return { text: this.maxLabel, showUnit: false };
109
+ }
110
+
111
+ return { text: formatNumber(currentValue), showUnit: true };
112
+ }
113
+
87
114
  constructor() {
88
115
  super();
89
116
  /** @type {ScopedStylesController} */
@@ -94,6 +121,8 @@ export class LionInputRange extends LocalizeMixin(LionInput) {
94
121
  this.unit = '';
95
122
  this.type = 'range';
96
123
  this.noMinMaxLabels = false;
124
+ this.minLabel = '';
125
+ this.maxLabel = '';
97
126
  /**
98
127
  * @param {string} modelValue
99
128
  */
@@ -115,6 +144,16 @@ export class LionInputRange extends LocalizeMixin(LionInput) {
115
144
  if (changedProperties.has('step')) {
116
145
  this._inputNode.step = `${this.step}`;
117
146
  }
147
+
148
+ if (changedProperties.has('modelValue')) {
149
+ if (this.minLabel && this.modelValue === this.min) {
150
+ this._inputNode.setAttribute('aria-valuetext', `${this.minLabel}`);
151
+ } else if (this.maxLabel && this.modelValue === this.max) {
152
+ this._inputNode.setAttribute('aria-valuetext', `${this.maxLabel}`);
153
+ } else {
154
+ this._inputNode.removeAttribute('aria-valuetext');
155
+ }
156
+ }
118
157
  }
119
158
 
120
159
  /** @param {import('lit').PropertyValues } changedProperties */
@@ -130,12 +169,12 @@ export class LionInputRange extends LocalizeMixin(LionInput) {
130
169
 
131
170
  /** @protected */
132
171
  _inputGroupTemplate() {
172
+ const display = this._valueDisplay;
173
+
133
174
  return html`
134
175
  <div>
135
- <span class="input-range__value"
136
- >${formatNumber(parseFloat(/** @type {string} */ (this.formattedValue)))}</span
137
- >
138
- <span class="input-range__unit">${this.unit}</span>
176
+ <span class="input-range__value">${display.text}</span>
177
+ ${display.showUnit ? html`<span class="input-range__unit">${this.unit}</span>` : ''}
139
178
  </div>
140
179
  <div class="input-group">
141
180
  ${this._inputGroupBeforeTemplate()}
@@ -157,12 +196,16 @@ export class LionInputRange extends LocalizeMixin(LionInput) {
157
196
  ? html`
158
197
  <div class="input-range__limits">
159
198
  <div>
160
- <span class="sr-only">${this.msgLit('lion-input-range:minimum')} </span
161
- >${formatNumber(this.min)}
199
+ <span class="sr-only">${this.msgLit('lion-input-range:minimum')} </span>${this
200
+ .minLabel
201
+ ? this.minLabel
202
+ : formatNumber(this.min)}
162
203
  </div>
163
204
  <div>
164
- <span class="sr-only">${this.msgLit('lion-input-range:maximum')} </span
165
- >${formatNumber(this.max)}
205
+ <span class="sr-only">${this.msgLit('lion-input-range:maximum')} </span>${this
206
+ .maxLabel
207
+ ? this.maxLabel
208
+ : formatNumber(this.max)}
166
209
  </div>
167
210
  </div>
168
211
  `
@@ -84,4 +84,118 @@ describe('<lion-input-range>', () => {
84
84
  const el = await fixture(`<lion-input-range label="range" disabled></lion-input-range>`);
85
85
  await expect(el).to.be.accessible();
86
86
  });
87
+
88
+ describe('Custom min/max labels', () => {
89
+ it('displays custom minLabel in the limits when provided', async () => {
90
+ const el = await fixture(html`
91
+ <lion-input-range min="0" max="100" min-label="Low" .modelValue=${50}></lion-input-range>
92
+ `);
93
+ const minDiv = /** @type {HTMLElement} */ (
94
+ /** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('.input-range__limits > div')[0]
95
+ );
96
+ expect(minDiv.textContent?.trim()).to.equal('Minimum Low');
97
+ });
98
+
99
+ it('displays custom maxLabel in the limits when provided', async () => {
100
+ const el = await fixture(html`
101
+ <lion-input-range min="0" max="100" max-label="High" .modelValue=${50}></lion-input-range>
102
+ `);
103
+ const maxDiv = /** @type {HTMLElement} */ (
104
+ /** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('.input-range__limits > div')[1]
105
+ );
106
+ expect(maxDiv.textContent?.trim()).to.equal('Maximum High');
107
+ });
108
+
109
+ it('displays custom minLabel as value display when value equals min', async () => {
110
+ const el = await fixture(html`
111
+ <lion-input-range min="0" max="100" min-label="Low" .modelValue=${0}></lion-input-range>
112
+ `);
113
+ const valueSpan = /** @type {HTMLElement} */ (
114
+ /** @type {ShadowRoot} */ (el.shadowRoot).querySelector('.input-range__value')
115
+ );
116
+ expect(valueSpan.innerText).to.equal('Low');
117
+ });
118
+
119
+ it('displays custom maxLabel as value display when value equals max', async () => {
120
+ const el = await fixture(html`
121
+ <lion-input-range min="0" max="100" max-label="High" .modelValue=${100}></lion-input-range>
122
+ `);
123
+ const valueSpan = /** @type {HTMLElement} */ (
124
+ /** @type {ShadowRoot} */ (el.shadowRoot).querySelector('.input-range__value')
125
+ );
126
+ expect(valueSpan.innerText).to.equal('High');
127
+ });
128
+
129
+ it('displays formatted number when value is between min and max', async () => {
130
+ const el = await fixture(html`
131
+ <lion-input-range
132
+ min="0"
133
+ max="100"
134
+ min-label="Low"
135
+ max-label="High"
136
+ .modelValue=${50}
137
+ ></lion-input-range>
138
+ `);
139
+ const valueSpan = /** @type {HTMLElement} */ (
140
+ /** @type {ShadowRoot} */ (el.shadowRoot).querySelector('.input-range__value')
141
+ );
142
+ expect(valueSpan.innerText).to.equal('50');
143
+ });
144
+
145
+ it('maintains numeric modelValue even when displaying custom label', async () => {
146
+ const el = await fixture(html`
147
+ <lion-input-range min="0" max="100" min-label="Low" .modelValue=${0}></lion-input-range>
148
+ `);
149
+ const valueSpan = /** @type {HTMLElement} */ (
150
+ /** @type {ShadowRoot} */ (el.shadowRoot).querySelector('.input-range__value')
151
+ );
152
+ // Display shows custom label
153
+ expect(valueSpan.innerText).to.equal('Low');
154
+ // But modelValue is still the number
155
+ expect(el.modelValue).to.equal(0);
156
+ expect(typeof el.modelValue).to.equal('number');
157
+ });
158
+
159
+ it('sets aria-valuetext to minLabel when modelValue equals min', async () => {
160
+ const el = await fixture(html`
161
+ <lion-input-range min="0" max="100" min-label="Low" .modelValue=${50}></lion-input-range>
162
+ `);
163
+
164
+ el.modelValue = 0;
165
+ await nextFrame();
166
+
167
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('Low');
168
+ });
169
+
170
+ it('sets aria-valuetext to maxLabel when modelValue equals max', async () => {
171
+ const el = await fixture(html`
172
+ <lion-input-range min="0" max="100" max-label="High" .modelValue=${50}></lion-input-range>
173
+ `);
174
+
175
+ el.modelValue = 100;
176
+ await nextFrame();
177
+
178
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('High');
179
+ });
180
+
181
+ it('removes aria-valuetext when modelValue is between min and max', async () => {
182
+ const el = await fixture(html`
183
+ <lion-input-range
184
+ min="0"
185
+ max="100"
186
+ min-label="Low"
187
+ max-label="High"
188
+ .modelValue=${0}
189
+ ></lion-input-range>
190
+ `);
191
+
192
+ await nextFrame();
193
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('Low');
194
+
195
+ el.modelValue = 50;
196
+ await nextFrame();
197
+
198
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.equal(false);
199
+ });
200
+ });
87
201
  });
@@ -1,4 +1,3 @@
1
- import { Unparseable } from '@lion/ui/form-core.js';
2
1
  import { LocalizeMixin } from '@lion/ui/localize-no-side-effects.js';
3
2
  import { LionInput } from '@lion/ui/input.js';
4
3
 
@@ -325,9 +324,7 @@ export class LionInputTel extends LocalizeMixin(LionInput) {
325
324
 
326
325
  // 2. Try to derive action region from user value
327
326
  const regex = /[+0-9]+/gi;
328
- const value = !(this.modelValue instanceof Unparseable)
329
- ? this.modelValue
330
- : this.value.match(regex)?.join('');
327
+ const value = this.value.match(regex)?.join('');
331
328
  const regionDerivedFromValue =
332
329
  value && this._phoneUtil && this._phoneUtil.parsePhoneNumber(value).regionCode;
333
330
 
@@ -1,5 +1,8 @@
1
1
  import { mimicUserChangingDropdown } from '@lion/ui/input-tel-dropdown-test-helpers.js';
2
- import { runInputTelDropdownSuite } from '@lion/ui/input-tel-dropdown-test-suites.js';
2
+ import {
3
+ runInputTelDropdownSuite,
4
+ runInputTelDropdownParenthesesSuite,
5
+ } from '@lion/ui/input-tel-dropdown-test-suites.js';
3
6
  import { LionInputTelDropdown } from '@lion/ui/input-tel-dropdown.js';
4
7
  import { runInputTelSuite } from '@lion/ui/input-tel-test-suites.js';
5
8
  import { aTimeout, expect, fixture } from '@open-wc/testing';
@@ -63,6 +66,7 @@ class WithFormControlInputTelDropdown extends ScopedElementsMixin(LionInputTelDr
63
66
  // @ts-expect-error
64
67
  runInputTelSuite({ klass: LionInputTelDropdown });
65
68
  runInputTelDropdownSuite();
69
+ runInputTelDropdownParenthesesSuite();
66
70
 
67
71
  describe('WithFormControlInputTelDropdown', () => {
68
72
  // @ts-expect-error