@lion/ui 0.9.0 → 0.10.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/combobox/src/LionCombobox.js +6 -2
- package/components/combobox/test/lion-combobox.test.js +58 -3
- package/components/core/test/ScopedElementsMixin.test.js +3 -3
- package/components/dialog/test/lion-dialog.test.js +54 -0
- package/components/dialog/test-helpers/test-router.js +43 -0
- package/components/form-core/src/FormatMixin.js +66 -18
- package/components/form-core/test-suites/FormatMixin.suite.js +107 -8
- package/components/form-core/types/FormatMixinTypes.ts +4 -1
- package/components/form-integrations/test/dialog-integrations.test.js +6 -2
- package/components/form-integrations/test/form-integrations.test.js +2 -2
- package/components/input-amount/src/parsers.js +3 -3
- package/components/input-amount/test/lion-input-amount.test.js +36 -2
- package/components/input-amount/test/parsers.test.js +11 -0
- package/components/input-datepicker/translations/es.js +1 -1
- package/components/input-stepper/src/LionInputStepper.js +70 -42
- package/components/input-stepper/test/lion-input-stepper.test.js +48 -12
- package/components/input-tel-dropdown/src/LionInputTelDropdown.js +7 -6
- package/components/input-tel-dropdown/test/LionInputTelDropdown.test.js +50 -0
- package/components/localize/src/number/getDecimalSeparator.js +1 -1
- package/components/localize/src/number/parseNumber.js +9 -6
- package/components/localize/test/number/parseNumber.test.js +11 -0
- package/components/localize/types/LocalizeMixinTypes.ts +3 -3
- package/components/overlays/src/OverlayController.js +4 -5
- package/components/overlays/src/OverlayMixin.js +20 -14
- package/components/overlays/test/OverlayController.test.js +11 -8
- package/components/overlays/test-suites/OverlayMixin.suite.js +184 -9
- package/components/overlays/types/ArrowMixinTypes.ts +11 -2
- package/components/overlays/types/OverlayConfig.ts +15 -13
- package/components/select-rich/src/LionSelectRich.js +3 -1
- package/components/select-rich/test/lion-select-rich.test.js +44 -0
- package/custom-elements.json +1337 -115
- package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
- package/dist-types/components/dialog/test/lion-dialog.test.d.ts.map +1 -1
- package/dist-types/components/dialog/test-helpers/test-router.d.ts +2 -0
- package/dist-types/components/dialog/test-helpers/test-router.d.ts.map +1 -0
- package/dist-types/components/form-core/src/FormatMixin.d.ts +1 -1
- package/dist-types/components/form-core/src/FormatMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts.map +1 -1
- package/dist-types/components/form-core/types/FormatMixinTypes.d.ts +3 -2
- package/dist-types/components/form-core/types/FormatMixinTypes.d.ts.map +1 -1
- package/dist-types/components/form-integrations/test/dialog-integrations.test.d.ts.map +1 -1
- package/dist-types/components/input-amount/src/parsers.d.ts +2 -2
- package/dist-types/components/input-amount/src/parsers.d.ts.map +1 -1
- package/dist-types/components/input-amount/test/lion-input-amount.test.d.ts.map +1 -1
- package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +22 -24
- package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
- package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts.map +1 -1
- package/dist-types/components/input-tel-dropdown/test/LionInputTelDropdown.test.d.ts.map +1 -1
- package/dist-types/components/localize/src/number/parseNumber.d.ts.map +1 -1
- package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts +3 -2
- package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
- package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts +4 -4
- package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
- package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts +2 -2
- package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
- package/dist-types/components/overlays/types/OverlayConfig.d.ts +4 -4
- package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
- package/dist-types/components/select-rich/test/lion-select-rich.test.d.ts.map +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/api-table.md +2 -2
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/api-table.md +1 -0
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/api-table.md +1 -0
- 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 +1 -1
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/api-table.md +1 -1
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-date/api-table.md +1 -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 +1 -1
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/api-table.md +1 -1
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-iban/api-table.md +1 -1
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/api-table.md +1 -1
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-stepper/api-table.md +4 -4
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-tel/api-table.md +1 -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 +1 -1
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/api-table.md +1 -1
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/radio-group/api-table.md +1 -1
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/api-table.md +1 -1
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/api-table.md +1 -0
- package/docs/components/select-rich/use-cases.md +1 -1
- package/docs/components/switch/api-table.md +1 -1
- package/docs/components/tabs/examples.md +1 -1
- package/docs/components/textarea/api-table.md +1 -1
- package/docs/components/tooltip/api-table.md +1 -0
- package/docs/fundamentals/index.md +4 -4
- package/docs/fundamentals/node-tools/providence-analytics/overview.md +5 -5
- 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 +1 -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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseNumber, getFractionDigits } from '@lion/ui/localize-no-side-effects.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions}
|
|
4
|
+
* @typedef {import('../../localize/types/LocalizeMixinTypes.js').FormatNumberOptions} FormatNumberOptions
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -28,7 +28,7 @@ function round(value, decimals) {
|
|
|
28
28
|
* parseAmount('1,234.56', {currency: 'JOD'}); => 1234.560
|
|
29
29
|
*
|
|
30
30
|
* @param {string} value Number to be parsed
|
|
31
|
-
* @param {
|
|
31
|
+
* @param {FormatNumberOptions} [givenOptions] Locale Options
|
|
32
32
|
*/
|
|
33
33
|
export function parseAmount(value, givenOptions) {
|
|
34
34
|
const unmatchedInput = value.match(/[^0-9,.\- ]/g);
|
|
@@ -44,7 +44,7 @@ export function parseAmount(value, givenOptions) {
|
|
|
44
44
|
return undefined;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** @type {
|
|
47
|
+
/** @type {FormatNumberOptions} */
|
|
48
48
|
const options = {
|
|
49
49
|
...givenOptions,
|
|
50
50
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { aTimeout, expect, fixture } from '@open-wc/testing';
|
|
2
2
|
import { html } from 'lit';
|
|
3
|
-
import {
|
|
3
|
+
import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
|
|
4
4
|
import { localizeTearDown } from '@lion/ui/localize-test-helpers.js';
|
|
5
5
|
import { getInputMembers } from '@lion/ui/input-test-helpers.js';
|
|
6
6
|
import { LionInputAmount, formatAmount, parseAmount } from '@lion/ui/input-amount.js';
|
|
7
|
-
|
|
7
|
+
import { mimicUserInput } from '@lion/ui/form-core-test-helpers.js';
|
|
8
|
+
import sinon from 'sinon';
|
|
8
9
|
import '@lion/ui/define/lion-input-amount.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -12,6 +13,8 @@ import '@lion/ui/define/lion-input-amount.js';
|
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
describe('<lion-input-amount>', () => {
|
|
16
|
+
const localize = getLocalizeManager();
|
|
17
|
+
|
|
15
18
|
beforeEach(() => {
|
|
16
19
|
localizeTearDown();
|
|
17
20
|
});
|
|
@@ -128,6 +131,37 @@ describe('<lion-input-amount>', () => {
|
|
|
128
131
|
expect(_inputNode.value).to.equal('100.12');
|
|
129
132
|
});
|
|
130
133
|
|
|
134
|
+
it('adjusts formats with locale when formatOptions.mode is "user-edited"', async () => {
|
|
135
|
+
const el = /** @type {LionInputAmount} */ (
|
|
136
|
+
await fixture(
|
|
137
|
+
html`<lion-input-amount
|
|
138
|
+
.modelValue=${123456.78}
|
|
139
|
+
currency="EUR"
|
|
140
|
+
.formatOptions="${{ locale: 'nl-NL' }}"
|
|
141
|
+
></lion-input-amount>`,
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
const parserSpy = sinon.spy(el, 'parser');
|
|
145
|
+
const formatterSpy = sinon.spy(el, 'formatter');
|
|
146
|
+
|
|
147
|
+
// @ts-expect-error [allow-protected] in test
|
|
148
|
+
expect(el._inputNode.value).to.equal('123.456,78');
|
|
149
|
+
|
|
150
|
+
// When editing an already existing value, we interpet the separators as they are
|
|
151
|
+
mimicUserInput(el, '123.456');
|
|
152
|
+
expect(parserSpy.lastCall.args[1]?.mode).to.equal('user-edited');
|
|
153
|
+
expect(formatterSpy.lastCall.args[1]?.mode).to.equal('user-edited');
|
|
154
|
+
expect(el.modelValue).to.equal(123456);
|
|
155
|
+
expect(el.formattedValue).to.equal('123.456,00');
|
|
156
|
+
|
|
157
|
+
// Formatting should only affect values that should be formatted / parsed as a consequence of user input.
|
|
158
|
+
// When a user finished editing, the default should be restored.
|
|
159
|
+
// (think of a programmatically set modelValue, that should behave idempotent, regardless of when it is set)
|
|
160
|
+
el.modelValue = 1234;
|
|
161
|
+
expect(el.formattedValue).to.equal('1.234,00');
|
|
162
|
+
expect(formatterSpy.lastCall.args[1]?.mode).to.equal('auto');
|
|
163
|
+
});
|
|
164
|
+
|
|
131
165
|
it('sets inputmode attribute to decimal', async () => {
|
|
132
166
|
const el = /** @type {LionInputAmount} */ (
|
|
133
167
|
await fixture(`<lion-input-amount></lion-input-amount>`)
|
|
@@ -60,4 +60,15 @@ describe('parseAmount()', async () => {
|
|
|
60
60
|
expect(parseAmount('foo1', { mode: 'pasted' })).to.equal(1);
|
|
61
61
|
expect(parseAmount('EUR 1,50', { mode: 'pasted' })).to.equal(1.5);
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
it('parses based on locale when "user-edited" mode used combined with viewValueStates "formatted"', async () => {
|
|
65
|
+
expect(parseAmount('123,456.78', { mode: 'auto' })).to.equal(123456.78);
|
|
66
|
+
expect(
|
|
67
|
+
parseAmount('123,456.78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
|
|
68
|
+
).to.equal(123456.78);
|
|
69
|
+
expect(parseAmount('123.456,78', { mode: 'auto' })).to.equal(123456.78);
|
|
70
|
+
expect(
|
|
71
|
+
parseAmount('123.456,78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
|
|
72
|
+
).to.equal(123.45678);
|
|
73
|
+
});
|
|
63
74
|
});
|
|
@@ -21,6 +21,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
21
21
|
.input-group__container > .input-group__input ::slotted(.form-control) {
|
|
22
22
|
text-align: center;
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
.input-stepper__value {
|
|
26
|
+
position: absolute;
|
|
27
|
+
width: 1px;
|
|
28
|
+
height: 1px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
clip-path: inset(100%);
|
|
31
|
+
clip: rect(1px, 1px, 1px, 1px);
|
|
32
|
+
white-space: nowrap;
|
|
33
|
+
border: 0;
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: 0;
|
|
36
|
+
}
|
|
24
37
|
`,
|
|
25
38
|
];
|
|
26
39
|
}
|
|
@@ -83,7 +96,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
83
96
|
|
|
84
97
|
this.__increment = this.__increment.bind(this);
|
|
85
98
|
this.__decrement = this.__decrement.bind(this);
|
|
86
|
-
this.
|
|
99
|
+
this._onEnterButton = this._onEnterButton.bind(this);
|
|
100
|
+
this._onLeaveButton = this._onLeaveButton.bind(this);
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
connectedCallback() {
|
|
@@ -93,11 +107,12 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
93
107
|
min: this.min,
|
|
94
108
|
step: this.step,
|
|
95
109
|
};
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
if (this._inputNode) {
|
|
111
|
+
this._inputNode.role = 'spinbutton';
|
|
112
|
+
this._inputNode.setAttribute('inputmode', 'decimal');
|
|
113
|
+
this._inputNode.setAttribute('autocomplete', 'off');
|
|
114
|
+
}
|
|
98
115
|
this.addEventListener('keydown', this.__keyDownHandler);
|
|
99
|
-
this._inputNode.setAttribute('inputmode', 'decimal');
|
|
100
|
-
this._inputNode.setAttribute('autocomplete', 'off');
|
|
101
116
|
this.__setDefaultValidators();
|
|
102
117
|
this.__toggleSpinnerButtonsState();
|
|
103
118
|
}
|
|
@@ -119,9 +134,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
119
134
|
this._inputNode.min = `${this.min}`;
|
|
120
135
|
this.values.min = this.min;
|
|
121
136
|
if (this.min !== Infinity) {
|
|
122
|
-
this.setAttribute('aria-valuemin', `${this.min}`);
|
|
137
|
+
this._inputNode.setAttribute('aria-valuemin', `${this.min}`);
|
|
123
138
|
} else {
|
|
124
|
-
this.removeAttribute('aria-valuemin');
|
|
139
|
+
this._inputNode.removeAttribute('aria-valuemin');
|
|
125
140
|
}
|
|
126
141
|
this.__toggleSpinnerButtonsState();
|
|
127
142
|
}
|
|
@@ -130,9 +145,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
130
145
|
this._inputNode.max = `${this.max}`;
|
|
131
146
|
this.values.max = this.max;
|
|
132
147
|
if (this.max !== Infinity) {
|
|
133
|
-
this.setAttribute('aria-valuemax', `${this.max}`);
|
|
148
|
+
this._inputNode.setAttribute('aria-valuemax', `${this.max}`);
|
|
134
149
|
} else {
|
|
135
|
-
this.removeAttribute('aria-valuemax');
|
|
150
|
+
this._inputNode.removeAttribute('aria-valuemax');
|
|
136
151
|
}
|
|
137
152
|
this.__toggleSpinnerButtonsState();
|
|
138
153
|
}
|
|
@@ -145,14 +160,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
145
160
|
this._inputNode.step = `${this.step}`;
|
|
146
161
|
this.values.step = this.step;
|
|
147
162
|
}
|
|
148
|
-
|
|
149
|
-
if (changedProperties.has('_ariaLabelledNodes')) {
|
|
150
|
-
this.__reflectAriaAttrToSpinButton('aria-labelledby', this._ariaLabelledNodes);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (changedProperties.has('_ariaDescribedNodes')) {
|
|
154
|
-
this.__reflectAriaAttrToSpinButton('aria-describedby', this._ariaDescribedNodes);
|
|
155
|
-
}
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
get slots() {
|
|
@@ -163,22 +170,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
163
170
|
};
|
|
164
171
|
}
|
|
165
172
|
|
|
166
|
-
/**
|
|
167
|
-
* Based on FormControlMixin __reflectAriaAttr()
|
|
168
|
-
*
|
|
169
|
-
* Will handle help text, validation feedback and character counter,
|
|
170
|
-
* prefix/suffix/before/after (if they contain data-description flag attr).
|
|
171
|
-
* Also, contents of id references that will be put in the <lion-field>._ariaDescribedby property
|
|
172
|
-
* from an external context, will be read by a screen reader.
|
|
173
|
-
* @param {string} attrName
|
|
174
|
-
* @param {Element[]} nodes
|
|
175
|
-
* @private
|
|
176
|
-
*/
|
|
177
|
-
__reflectAriaAttrToSpinButton(attrName, nodes) {
|
|
178
|
-
const string = nodes.map(n => n.id).join(' ');
|
|
179
|
-
this.setAttribute(attrName, string);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
173
|
/**
|
|
183
174
|
* Set aria labels and apply validators
|
|
184
175
|
* @private
|
|
@@ -227,7 +218,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
227
218
|
}
|
|
228
219
|
decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
|
|
229
220
|
incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
|
|
230
|
-
|
|
231
221
|
this._updateAriaAttributes();
|
|
232
222
|
}
|
|
233
223
|
|
|
@@ -237,21 +227,22 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
237
227
|
_updateAriaAttributes() {
|
|
238
228
|
const displayValue = this._inputNode.value;
|
|
239
229
|
if (displayValue) {
|
|
240
|
-
this.setAttribute('aria-valuenow', `${displayValue}`);
|
|
230
|
+
this._inputNode.setAttribute('aria-valuenow', `${displayValue}`);
|
|
241
231
|
if (
|
|
242
232
|
Object.keys(this.valueTextMapping).length !== 0 &&
|
|
243
233
|
Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue)
|
|
244
234
|
) {
|
|
245
|
-
this.
|
|
235
|
+
this.__valueText = this.valueTextMapping[this.currentValue];
|
|
246
236
|
} else {
|
|
247
237
|
// VoiceOver announces percentages once the valuemin or valuemax are used.
|
|
248
238
|
// This can be fixed by setting valuetext to the same value as valuenow
|
|
249
239
|
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
|
|
250
|
-
this.
|
|
240
|
+
this.__valueText = displayValue;
|
|
251
241
|
}
|
|
242
|
+
this._inputNode.setAttribute('aria-valuetext', `${this.__valueText}`);
|
|
252
243
|
} else {
|
|
253
|
-
this.removeAttribute('aria-valuenow');
|
|
254
|
-
this.removeAttribute('aria-valuetext');
|
|
244
|
+
this._inputNode.removeAttribute('aria-valuenow');
|
|
245
|
+
this._inputNode.removeAttribute('aria-valuetext');
|
|
255
246
|
}
|
|
256
247
|
}
|
|
257
248
|
|
|
@@ -373,7 +364,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
373
364
|
<button
|
|
374
365
|
?disabled=${this.disabled || this.readOnly}
|
|
375
366
|
@click=${this.__decrement}
|
|
376
|
-
@
|
|
367
|
+
@focus=${this._onEnterButton}
|
|
368
|
+
@blur=${this._onLeaveButton}
|
|
377
369
|
type="button"
|
|
378
370
|
aria-label="${this.msgLit('lion-input-stepper:decrease')}"
|
|
379
371
|
>
|
|
@@ -392,7 +384,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
392
384
|
<button
|
|
393
385
|
?disabled=${this.disabled || this.readOnly}
|
|
394
386
|
@click=${this.__increment}
|
|
395
|
-
@
|
|
387
|
+
@focus=${this._onEnterButton}
|
|
388
|
+
@blur=${this._onLeaveButton}
|
|
396
389
|
type="button"
|
|
397
390
|
aria-label="${this.msgLit('lion-input-stepper:increase')}"
|
|
398
391
|
>
|
|
@@ -401,6 +394,33 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
401
394
|
`;
|
|
402
395
|
}
|
|
403
396
|
|
|
397
|
+
/** @protected */
|
|
398
|
+
_inputGroupTemplate() {
|
|
399
|
+
return html`
|
|
400
|
+
<div class="input-stepper__value">${this.__valueText}</div>
|
|
401
|
+
<div class="input-group">
|
|
402
|
+
${this._inputGroupBeforeTemplate()}
|
|
403
|
+
<div class="input-group__container">
|
|
404
|
+
${this._inputGroupPrefixTemplate()} ${this._inputGroupInputTemplate()}
|
|
405
|
+
${this._inputGroupSuffixTemplate()}
|
|
406
|
+
</div>
|
|
407
|
+
${this._inputGroupAfterTemplate()}
|
|
408
|
+
</div>
|
|
409
|
+
`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @protected
|
|
414
|
+
* @param {Event} ev
|
|
415
|
+
*/
|
|
416
|
+
// eslint-disable-next-line no-unused-vars
|
|
417
|
+
_onEnterButton(ev) {
|
|
418
|
+
const valueNode = /** @type {HTMLElement} */ (
|
|
419
|
+
this.shadowRoot?.querySelector('.input-stepper__value')
|
|
420
|
+
);
|
|
421
|
+
valueNode.setAttribute('aria-live', 'assertive');
|
|
422
|
+
}
|
|
423
|
+
|
|
404
424
|
/**
|
|
405
425
|
* Redispatch leave event on host when catching leave event
|
|
406
426
|
* on the incrementor and decrementor button.
|
|
@@ -410,8 +430,16 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|
|
410
430
|
*
|
|
411
431
|
* Interacting with the buttons is "user interactions"
|
|
412
432
|
* the same way as focusing + blurring the field (native input)
|
|
433
|
+
*
|
|
434
|
+
* @protected
|
|
435
|
+
* @param {Event} ev
|
|
413
436
|
*/
|
|
414
|
-
|
|
437
|
+
// eslint-disable-next-line no-unused-vars
|
|
438
|
+
_onLeaveButton(ev) {
|
|
439
|
+
const valueNode = /** @type {HTMLElement} */ (
|
|
440
|
+
this.shadowRoot?.querySelector('.input-stepper__value')
|
|
441
|
+
);
|
|
442
|
+
valueNode.removeAttribute('aria-live');
|
|
415
443
|
this.dispatchEvent(new Event(this._leaveEvent));
|
|
416
444
|
}
|
|
417
445
|
}
|
|
@@ -278,17 +278,23 @@ describe('<lion-input-stepper>', () => {
|
|
|
278
278
|
await expect(el).to.be.accessible();
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
+
it('has role="spinbutton"', async () => {
|
|
282
|
+
const el = await fixture(defaultInputStepper);
|
|
283
|
+
expect(el._inputNode.hasAttribute('role')).to.be.true;
|
|
284
|
+
expect(el._inputNode.getAttribute('role')).to.equal('spinbutton');
|
|
285
|
+
});
|
|
286
|
+
|
|
281
287
|
it('updates aria-valuenow when stepper is changed', async () => {
|
|
282
288
|
const el = await fixture(defaultInputStepper);
|
|
283
289
|
el.modelValue = 1;
|
|
284
290
|
|
|
285
291
|
await el.updateComplete;
|
|
286
|
-
expect(el.hasAttribute('aria-valuenow')).to.be.true;
|
|
287
|
-
expect(el.getAttribute('aria-valuenow')).to.equal('1');
|
|
292
|
+
expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.true;
|
|
293
|
+
expect(el._inputNode.getAttribute('aria-valuenow')).to.equal('1');
|
|
288
294
|
|
|
289
295
|
el.modelValue = '';
|
|
290
296
|
await el.updateComplete;
|
|
291
|
-
expect(el.hasAttribute('aria-valuenow')).to.be.false;
|
|
297
|
+
expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.false;
|
|
292
298
|
});
|
|
293
299
|
|
|
294
300
|
it('updates aria-valuetext when stepper is changed', async () => {
|
|
@@ -299,12 +305,12 @@ describe('<lion-input-stepper>', () => {
|
|
|
299
305
|
el.modelValue = 1;
|
|
300
306
|
await el.updateComplete;
|
|
301
307
|
|
|
302
|
-
expect(el.hasAttribute('aria-valuetext')).to.be.true;
|
|
303
|
-
expect(el.getAttribute('aria-valuetext')).to.equal('1');
|
|
308
|
+
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
|
|
309
|
+
expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('1');
|
|
304
310
|
|
|
305
311
|
el.modelValue = '';
|
|
306
312
|
await el.updateComplete;
|
|
307
|
-
expect(el.hasAttribute('aria-valuetext')).to.be.false;
|
|
313
|
+
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.false;
|
|
308
314
|
});
|
|
309
315
|
|
|
310
316
|
it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
|
|
@@ -318,30 +324,60 @@ describe('<lion-input-stepper>', () => {
|
|
|
318
324
|
`);
|
|
319
325
|
el.modelValue = 1;
|
|
320
326
|
await el.updateComplete;
|
|
321
|
-
expect(el.hasAttribute('aria-valuetext')).to.be.true;
|
|
322
|
-
expect(el.getAttribute('aria-valuetext')).to.equal('first');
|
|
327
|
+
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
|
|
328
|
+
expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('first');
|
|
323
329
|
});
|
|
324
330
|
|
|
325
331
|
it('updates aria-valuemin when stepper is changed', async () => {
|
|
326
332
|
const el = await fixture(inputStepperWithAttrs);
|
|
327
333
|
const incrementButton = el.querySelector('[slot=suffix]');
|
|
328
334
|
incrementButton?.dispatchEvent(new Event('click'));
|
|
329
|
-
expect(el
|
|
335
|
+
expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
|
|
336
|
+
expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('100');
|
|
330
337
|
|
|
331
338
|
el.min = 0;
|
|
332
339
|
await el.updateComplete;
|
|
333
|
-
expect(el
|
|
340
|
+
expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
|
|
341
|
+
expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('0');
|
|
334
342
|
});
|
|
335
343
|
|
|
336
344
|
it('updates aria-valuemax when stepper is changed', async () => {
|
|
337
345
|
const el = await fixture(inputStepperWithAttrs);
|
|
338
346
|
const incrementButton = el.querySelector('[slot=suffix]');
|
|
339
347
|
incrementButton?.dispatchEvent(new Event('click'));
|
|
340
|
-
expect(el
|
|
348
|
+
expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
|
|
349
|
+
expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('200');
|
|
341
350
|
|
|
342
351
|
el.max = 1000;
|
|
343
352
|
await el.updateComplete;
|
|
344
|
-
expect(el
|
|
353
|
+
expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
|
|
354
|
+
expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('when decrease button gets focus, it sets aria-live to input-stepper__value', async () => {
|
|
358
|
+
const el = await fixture(inputStepperWithAttrs);
|
|
359
|
+
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
|
|
360
|
+
const decrementButton = el.querySelector('[slot=prefix]');
|
|
361
|
+
|
|
362
|
+
decrementButton?.dispatchEvent(new Event('focus'));
|
|
363
|
+
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
|
|
364
|
+
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
|
|
365
|
+
|
|
366
|
+
decrementButton?.dispatchEvent(new Event('blur'));
|
|
367
|
+
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('when increase button gets focus, it sets aria-live to input-stepper__value', async () => {
|
|
371
|
+
const el = await fixture(inputStepperWithAttrs);
|
|
372
|
+
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
|
|
373
|
+
const incrementButton = el.querySelector('[slot=suffix]');
|
|
374
|
+
|
|
375
|
+
incrementButton?.dispatchEvent(new Event('focus'));
|
|
376
|
+
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
|
|
377
|
+
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
|
|
378
|
+
|
|
379
|
+
incrementButton?.dispatchEvent(new Event('blur'));
|
|
380
|
+
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
|
|
345
381
|
});
|
|
346
382
|
});
|
|
347
383
|
});
|
|
@@ -158,11 +158,11 @@ export class LionInputTelDropdown extends LionInputTel {
|
|
|
158
158
|
super.styles,
|
|
159
159
|
css`
|
|
160
160
|
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
* We need to align the height of the dropdown with the height of the text field.
|
|
162
|
+
* We target the HTMLDivElement (render wrapper from SlotMixin) here. Its child,
|
|
163
|
+
* [data-ref=dropdown], receives a 100% height as well via inline styles (since we
|
|
164
|
+
* can't target from shadow styles).
|
|
165
|
+
*/
|
|
166
166
|
::slotted([slot='prefix']) {
|
|
167
167
|
height: 100%;
|
|
168
168
|
}
|
|
@@ -301,7 +301,8 @@ export class LionInputTelDropdown extends LionInputTel {
|
|
|
301
301
|
} else {
|
|
302
302
|
this.__initializedRegionCode = `+${countryCode}`;
|
|
303
303
|
}
|
|
304
|
-
|
|
304
|
+
// on init we set the value to the _inputNode and not the modelValue to prevent validation
|
|
305
|
+
this._inputNode.value = this.__initializedRegionCode;
|
|
305
306
|
this._initialModelValue = this.__initializedRegionCode;
|
|
306
307
|
this.initInteractionState();
|
|
307
308
|
}
|
|
@@ -9,8 +9,10 @@ import { LionOption } from '@lion/ui/listbox.js';
|
|
|
9
9
|
import { ref } from 'lit/directives/ref.js';
|
|
10
10
|
import { html } from 'lit';
|
|
11
11
|
|
|
12
|
+
import { mockPhoneUtilManager, restorePhoneUtilManager } from '@lion/ui/input-tel-test-helpers.js';
|
|
12
13
|
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
|
13
14
|
import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
|
|
15
|
+
import '@lion/ui/define/lion-input-tel-dropdown.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* @typedef {import('../types/index.js').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
|
|
@@ -67,6 +69,18 @@ describe('WithFormControlInputTelDropdown', () => {
|
|
|
67
69
|
// Runs it for LionSelectRich, which uses .modelValue/@model-value-changed instead of .value/@change
|
|
68
70
|
runInputTelDropdownSuite({ klass: WithFormControlInputTelDropdown });
|
|
69
71
|
|
|
72
|
+
it("it doesn't set the country as modelValue, only as viewValue", async () => {
|
|
73
|
+
const el = /** @type {LionInputTelDropdown} */ (
|
|
74
|
+
await fixture(html`
|
|
75
|
+
<lion-input-tel-dropdown .allowedRegions="${['NL']}"></lion-input-tel-dropdown>
|
|
76
|
+
`)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// @ts-expect-error [allow-protected-in-tests]
|
|
80
|
+
expect(el._inputNode.value).to.equal('+31');
|
|
81
|
+
expect(el.modelValue).to.equal('');
|
|
82
|
+
});
|
|
83
|
+
|
|
70
84
|
it('focuses the textbox right after selection if selected via opened dropdown if interaction-mode is mac', async () => {
|
|
71
85
|
class InputTelDropdownMac extends LionInputTelDropdown {
|
|
72
86
|
static templates = {
|
|
@@ -172,4 +186,40 @@ describe('WithFormControlInputTelDropdown', () => {
|
|
|
172
186
|
expect(isActiveElement(el._inputNode)).to.be.false;
|
|
173
187
|
}
|
|
174
188
|
});
|
|
189
|
+
|
|
190
|
+
describe('defaultValidators', () => {
|
|
191
|
+
/** @type {(value:any) => void} */
|
|
192
|
+
let resolveLoaded;
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
({ resolveLoaded } = mockPhoneUtilManager());
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
restorePhoneUtilManager();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('without interaction are not called', async () => {
|
|
202
|
+
const el = /** @type {LionInputTelDropdown} */ (
|
|
203
|
+
await fixture(html`
|
|
204
|
+
<lion-input-tel-dropdown .allowedRegions="${['NL']}"></lion-input-tel-dropdown>
|
|
205
|
+
`)
|
|
206
|
+
);
|
|
207
|
+
resolveLoaded(undefined);
|
|
208
|
+
await aTimeout(0);
|
|
209
|
+
expect(el.hasFeedbackFor).to.deep.equal([]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('with interaction are called', async () => {
|
|
213
|
+
const el = /** @type {LionInputTelDropdown} */ (
|
|
214
|
+
await fixture(html`
|
|
215
|
+
<lion-input-tel-dropdown .allowedRegions="${['NL']}"></lion-input-tel-dropdown>
|
|
216
|
+
`)
|
|
217
|
+
);
|
|
218
|
+
el.modelValue = '+31 6';
|
|
219
|
+
|
|
220
|
+
resolveLoaded(undefined);
|
|
221
|
+
await aTimeout(0);
|
|
222
|
+
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
175
225
|
});
|
|
@@ -8,7 +8,7 @@ import { getLocale } from '../utils/getLocale.js';
|
|
|
8
8
|
* @returns {string} The separator
|
|
9
9
|
*/
|
|
10
10
|
export function getDecimalSeparator(locale, options) {
|
|
11
|
-
if (options
|
|
11
|
+
if (options?.decimalSeparator) {
|
|
12
12
|
return options.decimalSeparator;
|
|
13
13
|
}
|
|
14
14
|
const computedLocale = getLocale(locale);
|
|
@@ -16,14 +16,18 @@ import { getDecimalSeparator } from './getDecimalSeparator.js';
|
|
|
16
16
|
* parseNumber('1,234.56'); // method: heuristic => 1234.56
|
|
17
17
|
*
|
|
18
18
|
* @param {string} value Clean number (only [0-9 ,.]) to be parsed
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {string?} [options.mode] auto|pasted
|
|
19
|
+
* @param {{mode?:'auto'|'pasted'|'user-edited'; viewValueStates?:string[]}} options
|
|
21
20
|
* @return {string} unparseable|withLocale|heuristic
|
|
22
21
|
*/
|
|
23
|
-
function getParseMode(value, { mode = 'auto' } = {}) {
|
|
22
|
+
function getParseMode(value, { mode = 'auto', viewValueStates } = {}) {
|
|
24
23
|
const separators = value.match(/[., ]/g);
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
// When a user edits an existing value, we already formatted it with a certain locale.
|
|
26
|
+
// For best UX, we stick with this locale
|
|
27
|
+
const shouldAlignWithExistingSeparators =
|
|
28
|
+
viewValueStates?.includes('formatted') && mode === 'user-edited';
|
|
29
|
+
|
|
30
|
+
if (!separators || shouldAlignWithExistingSeparators) {
|
|
27
31
|
return 'withLocale';
|
|
28
32
|
}
|
|
29
33
|
if (mode === 'auto' && separators.length === 1) {
|
|
@@ -52,8 +56,7 @@ function getParseMode(value, { mode = 'auto' } = {}) {
|
|
|
52
56
|
* @param {import('../../types/LocalizeMixinTypes.js').FormatNumberOptions} options Locale Options
|
|
53
57
|
*/
|
|
54
58
|
function parseWithLocale(value, options) {
|
|
55
|
-
const
|
|
56
|
-
const separator = getDecimalSeparator(locale, options);
|
|
59
|
+
const separator = getDecimalSeparator(options?.locale, options);
|
|
57
60
|
const regexNumberAndLocaleSeparator = new RegExp(`[0-9${separator}-]`, 'g');
|
|
58
61
|
let numberAndLocaleSeparator = value.match(regexNumberAndLocaleSeparator)?.join('');
|
|
59
62
|
if (separator === ',') {
|
|
@@ -74,6 +74,17 @@ describe('parseNumber()', () => {
|
|
|
74
74
|
expect(parseNumber('123456.78', { mode: 'pasted' })).to.equal(123456.78);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it('detects separators withLocale when "user-edited" mode combined with viewValueStates "formatted" used e.g. 123.456,78', async () => {
|
|
78
|
+
expect(parseNumber('123,456.78', { mode: 'auto' })).to.equal(123456.78);
|
|
79
|
+
expect(
|
|
80
|
+
parseNumber('123,456.78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
|
|
81
|
+
).to.equal(123456.78);
|
|
82
|
+
expect(parseNumber('123.456,78', { mode: 'auto' })).to.equal(123456.78);
|
|
83
|
+
expect(
|
|
84
|
+
parseNumber('123.456,78', { mode: 'user-edited', viewValueStates: ['formatted'] }),
|
|
85
|
+
).to.equal(123.45678);
|
|
86
|
+
});
|
|
87
|
+
|
|
77
88
|
it('detects separators unparseable when there are 2 same ones e.g. 1.234.56', () => {
|
|
78
89
|
expect(parseNumber('1.234.56')).to.equal(123456);
|
|
79
90
|
expect(parseNumber('1,234,56')).to.equal(123456);
|
|
@@ -21,7 +21,7 @@ export declare interface FormatDateOptions extends Intl.DateTimeFormatOptions {
|
|
|
21
21
|
|
|
22
22
|
roundMode?: string;
|
|
23
23
|
returnIfNaN?: string;
|
|
24
|
-
mode?: 'pasted' | 'auto';
|
|
24
|
+
mode?: 'pasted' | 'auto' | 'user-edited';
|
|
25
25
|
|
|
26
26
|
postProcessors?: Map<string, DatePostProcessor>;
|
|
27
27
|
}
|
|
@@ -41,8 +41,8 @@ export declare interface FormatNumberOptions extends Intl.NumberFormatOptions {
|
|
|
41
41
|
// https://en.wikipedia.org/wiki/Decimal_separator#Digit_grouping
|
|
42
42
|
// note the half space in there as well
|
|
43
43
|
groupSeparator?: ',' | '.' | ' ' | '_' | ' ' | "'";
|
|
44
|
-
mode?: 'pasted' | 'auto';
|
|
45
|
-
|
|
44
|
+
mode?: 'pasted' | 'auto' | 'user-edited';
|
|
45
|
+
viewValueStates?: 'formatted'[];
|
|
46
46
|
postProcessors?: Map<string, NumberPostProcessor>;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -1191,9 +1191,8 @@ export class OverlayController extends EventTarget {
|
|
|
1191
1191
|
const hasPressedInside =
|
|
1192
1192
|
event.composedPath().includes(this.contentNode) ||
|
|
1193
1193
|
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
}
|
|
1194
|
+
if (hasPressedInside) return;
|
|
1195
|
+
this.hide();
|
|
1197
1196
|
};
|
|
1198
1197
|
|
|
1199
1198
|
/**
|
|
@@ -1206,7 +1205,7 @@ export class OverlayController extends EventTarget {
|
|
|
1206
1205
|
if (this.invokerNode) {
|
|
1207
1206
|
this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
|
|
1208
1207
|
}
|
|
1209
|
-
} else if (phase === 'hide') {
|
|
1208
|
+
} else if (phase === 'hide' || phase === 'teardown') {
|
|
1210
1209
|
this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
|
|
1211
1210
|
if (this.invokerNode) {
|
|
1212
1211
|
this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
|
|
@@ -1221,7 +1220,7 @@ export class OverlayController extends EventTarget {
|
|
|
1221
1220
|
_handleHidesOnOutsideEsc({ phase }) {
|
|
1222
1221
|
if (phase === 'show') {
|
|
1223
1222
|
document.addEventListener('keyup', this.#outsideEscKeyHandler);
|
|
1224
|
-
} else if (phase === 'hide') {
|
|
1223
|
+
} else if (phase === 'hide' || phase === 'teardown') {
|
|
1225
1224
|
document.removeEventListener('keyup', this.#outsideEscKeyHandler);
|
|
1226
1225
|
}
|
|
1227
1226
|
}
|