@lion/ui 0.20.0 → 0.21.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.
- package/components/form-core/src/FormatMixin.js +10 -5
- package/components/form-core/test-helpers/mimicUserInput.js +1 -0
- package/components/form-core/test-suites/FormatMixin.suite.js +31 -14
- package/components/input-amount-dropdown/src/LionInputAmountDropdown.js +51 -8
- package/components/input-amount-dropdown/test/lion-input-amount-dropdown-integrations.test.js +5 -0
- package/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.js +1 -0
- package/components/input-datepicker/src/LionInputDatepicker.js +4 -0
- package/components/input-datepicker/test/lion-input-datepicker.test.js +55 -0
- package/components/input-range/src/LionInputRange.js +51 -8
- package/components/input-range/test/lion-input-range.test.js +114 -0
- package/custom-elements.json +415 -96
- package/dist-types/components/form-core/src/FormatMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/test-helpers/mimicUserInput.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts +8 -0
- package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts.map +1 -1
- package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts +24 -6
- package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts.map +1 -1
- package/dist-types/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.d.ts.map +1 -1
- package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -0
- package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts.map +1 -1
- package/dist-types/components/input-datepicker/test/lion-input-datepicker.test.d.ts.map +1 -1
- package/dist-types/components/input-range/src/LionInputRange.d.ts +11 -0
- package/dist-types/components/input-range/src/LionInputRange.d.ts.map +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/api-table.md +4 -2
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/overview.md +1 -1
- package/docs/components/fieldset/overview.md +5 -5
- package/docs/components/form/overview.md +3 -3
- package/docs/components/form/use-cases.md +2 -2
- package/docs/components/icon/use-cases.md +1 -1
- package/docs/components/input/api-table.md +2 -1
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/api-table.md +2 -1
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-amount-dropdown/api-table.md +4 -3
- package/docs/components/input-date/api-table.md +2 -1
- package/docs/components/input-date/overview.md +3 -3
- package/docs/components/input-datepicker/api-table.md +2 -1
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/api-table.md +2 -1
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/api-table.md +2 -1
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-iban/api-table.md +2 -1
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/api-table.md +7 -1
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-range/use-cases.md +14 -0
- package/docs/components/input-stepper/api-table.md +2 -1
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-tel/api-table.md +2 -1
- package/docs/components/input-tel/overview.md +1 -1
- package/docs/components/input-tel/use-cases.md +6 -6
- package/docs/components/input-tel-dropdown/api-table.md +2 -1
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/api-table.md +2 -1
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/radio-group/api-table.md +2 -1
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/api-table.md +2 -1
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/use-cases.md +2 -2
- package/docs/components/switch/api-table.md +2 -1
- package/docs/components/tabs/examples.md +1 -1
- package/docs/components/textarea/api-table.md +2 -1
- package/docs/fundamentals/index.md +4 -4
- package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
- package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
- package/docs/fundamentals/rationales/accessibility.md +1 -1
- package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
- package/docs/fundamentals/systems/core/overview.md +1 -1
- package/docs/fundamentals/systems/form/api-table.md +2 -1
- package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
- package/docs/fundamentals/systems/form/model-value.md +1 -1
- package/docs/fundamentals/systems/form/overview.md +28 -28
- package/docs/fundamentals/systems/form/validate.md +2 -2
- package/docs/fundamentals/systems/localize/overview.md +4 -4
- package/docs/fundamentals/systems/overlays/configuration.md +2 -2
- package/docs/fundamentals/systems/overlays/overview.md +5 -5
- package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
- package/docs/fundamentals/tools/helpers/overview.md +1 -1
- package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
- package/docs/guides/how-to/create-a-custom-field.md +5 -5
- package/docs/guides/how-to/create-a-form.md +4 -4
- package/docs/guides/how-to/extend-a-native-input.md +2 -2
- package/docs/guides/how-to/index.md +1 -1
- package/docs/guides/index.md +8 -8
- package/docs/guides/principles/index.md +5 -5
- package/docs/guides/principles/styling.md +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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="${
|
|
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=${
|
|
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
|
-
|
|
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(
|
|
399
|
+
expect(formatElem._inputNode.value).to.equal(expectedFormattedValue);
|
|
384
400
|
|
|
385
|
-
expect(formatElem.serializedValue).to.equal(
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|
package/components/input-amount-dropdown/test/lion-input-amount-dropdown-integrations.test.js
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|