@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
|
@@ -663,7 +663,11 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|
|
663
663
|
) {
|
|
664
664
|
return false;
|
|
665
665
|
}
|
|
666
|
-
if (
|
|
666
|
+
if (
|
|
667
|
+
this.filled ||
|
|
668
|
+
this.showAllOnEmpty ||
|
|
669
|
+
(!this.filled && this.multipleChoice && this.__prevCboxValueNonSelected)
|
|
670
|
+
) {
|
|
667
671
|
return true;
|
|
668
672
|
}
|
|
669
673
|
// when no keyboard action involved (on focused change), return current opened state
|
|
@@ -705,7 +709,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|
|
705
709
|
// eslint-disable-next-line no-unused-vars
|
|
706
710
|
_textboxOnInput(ev) {
|
|
707
711
|
this.__shouldAutocompleteNextUpdate = true;
|
|
708
|
-
this.opened =
|
|
712
|
+
this.opened = this._showOverlayCondition({});
|
|
709
713
|
}
|
|
710
714
|
|
|
711
715
|
/**
|
|
@@ -229,6 +229,8 @@ describe('lion-combobox', () => {
|
|
|
229
229
|
</lion-combobox>
|
|
230
230
|
`)
|
|
231
231
|
);
|
|
232
|
+
await el.updateComplete;
|
|
233
|
+
|
|
232
234
|
const { _inputNode } = getComboboxMembers(el);
|
|
233
235
|
|
|
234
236
|
_inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }));
|
|
@@ -931,6 +933,50 @@ describe('lion-combobox', () => {
|
|
|
931
933
|
expect(el.opened).to.equal(false);
|
|
932
934
|
});
|
|
933
935
|
|
|
936
|
+
it('does not flash the menu when _showOverlayCondition returns "false"', async () => {
|
|
937
|
+
class ComplexCombobox extends LionCombobox {
|
|
938
|
+
_showOverlayCondition() {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const tagName = defineCE(ComplexCombobox);
|
|
944
|
+
const tag = unsafeStatic(tagName);
|
|
945
|
+
|
|
946
|
+
const el = /** @type {LionCombobox} */ (
|
|
947
|
+
await fixture(html`
|
|
948
|
+
<${tag} name="combo" label="Display only the label once selected">
|
|
949
|
+
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
|
950
|
+
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
|
951
|
+
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
|
952
|
+
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
|
953
|
+
</${tag}>
|
|
954
|
+
`)
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
const dialog = el.shadowRoot?.querySelector('dialog');
|
|
958
|
+
/**
|
|
959
|
+
* hasDropdownFlashed is `true` if the menu was shown for a short period of time and then got closed
|
|
960
|
+
*/
|
|
961
|
+
let hasDropdownFlashed = false;
|
|
962
|
+
const observer = new MutationObserver(mutationList => {
|
|
963
|
+
// eslint-disable-next-line no-unused-vars
|
|
964
|
+
for (const mutation of mutationList) {
|
|
965
|
+
if (dialog?.style.display === '') {
|
|
966
|
+
hasDropdownFlashed = true;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
observer.observe(/** @type {Node} */ (dialog), { attributeFilter: ['style'] });
|
|
971
|
+
|
|
972
|
+
const { _inputNode } = getComboboxMembers(el);
|
|
973
|
+
_inputNode.focus();
|
|
974
|
+
await sendKeys({
|
|
975
|
+
type: 'art',
|
|
976
|
+
});
|
|
977
|
+
expect(hasDropdownFlashed).to.be.false;
|
|
978
|
+
});
|
|
979
|
+
|
|
934
980
|
it('shows overlay on click when filled', async () => {
|
|
935
981
|
const el = /** @type {LionCombobox} */ (
|
|
936
982
|
await fixture(html`
|
|
@@ -1157,7 +1203,10 @@ describe('lion-combobox', () => {
|
|
|
1157
1203
|
class ShowOverlayConditionCombobox extends LionCombobox {
|
|
1158
1204
|
/** @param {{ currentValue: string, lastKey:string }} options */
|
|
1159
1205
|
_showOverlayCondition(options) {
|
|
1160
|
-
return
|
|
1206
|
+
return (
|
|
1207
|
+
// @ts-ignore
|
|
1208
|
+
this.__prevCboxValueNonSelected.length > 3 && super._showOverlayCondition(options)
|
|
1209
|
+
);
|
|
1161
1210
|
}
|
|
1162
1211
|
}
|
|
1163
1212
|
const tagName = defineCE(ShowOverlayConditionCombobox);
|
|
@@ -1182,7 +1231,10 @@ describe('lion-combobox', () => {
|
|
|
1182
1231
|
class ShowOverlayConditionCombobox extends LionCombobox {
|
|
1183
1232
|
/** @param {{ currentValue: string, lastKey:string }} options */
|
|
1184
1233
|
_showOverlayCondition(options) {
|
|
1185
|
-
return
|
|
1234
|
+
return (
|
|
1235
|
+
// @ts-ignore
|
|
1236
|
+
this.__prevCboxValueNonSelected.length > 3 && super._showOverlayCondition(options)
|
|
1237
|
+
);
|
|
1186
1238
|
}
|
|
1187
1239
|
}
|
|
1188
1240
|
const tagName = defineCE(ShowOverlayConditionCombobox);
|
|
@@ -1207,7 +1259,10 @@ describe('lion-combobox', () => {
|
|
|
1207
1259
|
class ShowOverlayConditionCombobox extends LionCombobox {
|
|
1208
1260
|
/** @param {{ currentValue: string, lastKey:string }} options */
|
|
1209
1261
|
_showOverlayCondition(options) {
|
|
1210
|
-
return
|
|
1262
|
+
return (
|
|
1263
|
+
// @ts-ignore
|
|
1264
|
+
this.__prevCboxValueNonSelected.length > 3 && super._showOverlayCondition(options)
|
|
1265
|
+
);
|
|
1211
1266
|
}
|
|
1212
1267
|
}
|
|
1213
1268
|
const tagName = defineCE(ShowOverlayConditionCombobox);
|
|
@@ -140,7 +140,7 @@ describe('ScopedElementsMixin', () => {
|
|
|
140
140
|
}
|
|
141
141
|
customElements.define('scoped-elements-host-no-reg-2', ScopedElementsHostNoReg2);
|
|
142
142
|
|
|
143
|
-
const
|
|
143
|
+
const errorStub = sinon.stub(console, 'error');
|
|
144
144
|
/** @type {ScopedElementsHostNoReg2} */ (
|
|
145
145
|
await fixture(html`<scoped-elements-host-no-reg></scoped-elements-host-no-reg>`)
|
|
146
146
|
);
|
|
@@ -148,7 +148,7 @@ describe('ScopedElementsMixin', () => {
|
|
|
148
148
|
await fixture(html`<scoped-elements-host-no-reg-2></scoped-elements-host-no-reg-2>`)
|
|
149
149
|
);
|
|
150
150
|
|
|
151
|
-
expect(
|
|
151
|
+
expect(errorStub.args[0][0]).to.equal(
|
|
152
152
|
[
|
|
153
153
|
'You are trying to re-register the "scoped-elements-child-no-reg" custom element with a different class via ScopedElementsMixin.',
|
|
154
154
|
'This is only possible with a CustomElementRegistry.',
|
|
@@ -158,7 +158,7 @@ describe('ScopedElementsMixin', () => {
|
|
|
158
158
|
'For more details you can visit https://open-wc.org/docs/development/scoped-elements/',
|
|
159
159
|
].join('\n'),
|
|
160
160
|
);
|
|
161
|
-
|
|
161
|
+
errorStub.restore();
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
164
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { expect, fixture as _fixture, html, unsafeStatic, aTimeout } from '@open-wc/testing';
|
|
3
3
|
import { runOverlayMixinSuite } from '../../overlays/test-suites/OverlayMixin.suite.js';
|
|
4
4
|
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
|
5
|
+
import '../test-helpers/test-router.js';
|
|
5
6
|
import '@lion/ui/define/lion-dialog.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -187,4 +188,57 @@ describe('lion-dialog', () => {
|
|
|
187
188
|
expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
|
|
188
189
|
});
|
|
189
190
|
});
|
|
191
|
+
|
|
192
|
+
describe('Edge cases', () => {
|
|
193
|
+
it('does not lose click event handler when was detached and reattached', async () => {
|
|
194
|
+
const el = await fixture(html`
|
|
195
|
+
<test-router
|
|
196
|
+
.routingMap="${{
|
|
197
|
+
a: html`
|
|
198
|
+
<lion-dialog>
|
|
199
|
+
<div slot="content" class="dialog">Hey there</div>
|
|
200
|
+
<button slot="invoker">Popup button</button>
|
|
201
|
+
</lion-dialog>
|
|
202
|
+
`,
|
|
203
|
+
b: html` <div>B</div> `,
|
|
204
|
+
}}"
|
|
205
|
+
path="a"
|
|
206
|
+
></test-router>
|
|
207
|
+
`);
|
|
208
|
+
|
|
209
|
+
const getDialog = () =>
|
|
210
|
+
/** @type {LionDialog} */ (el.shadowRoot?.querySelector('lion-dialog'));
|
|
211
|
+
const getInvoker = () =>
|
|
212
|
+
/** @type {HTMLElement} */ (getDialog().querySelector('[slot="invoker"]'));
|
|
213
|
+
|
|
214
|
+
getInvoker().click();
|
|
215
|
+
const dialog = getDialog();
|
|
216
|
+
// @ts-expect-error [allow-protected-in-tests]
|
|
217
|
+
await dialog._overlayCtrl._showComplete;
|
|
218
|
+
expect(dialog.opened).to.be.true;
|
|
219
|
+
|
|
220
|
+
dialog.close();
|
|
221
|
+
// @ts-expect-error [allow-protected-in-tests]
|
|
222
|
+
await dialog._overlayCtrl._hideComplete;
|
|
223
|
+
expect(dialog.opened).to.be.false;
|
|
224
|
+
|
|
225
|
+
const buttonA = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('#path-a'));
|
|
226
|
+
const buttonB = /** @type {HTMLElement} */ (el.shadowRoot?.querySelector('#path-b'));
|
|
227
|
+
|
|
228
|
+
buttonB.click();
|
|
229
|
+
await el.updateComplete;
|
|
230
|
+
buttonA.click();
|
|
231
|
+
await el.updateComplete;
|
|
232
|
+
await el.updateComplete;
|
|
233
|
+
|
|
234
|
+
getInvoker().click();
|
|
235
|
+
|
|
236
|
+
const dialogAfterRouteChange = getDialog();
|
|
237
|
+
// @ts-expect-error [allow-protected-in-tests]
|
|
238
|
+
expect(dialogAfterRouteChange._overlayCtrl).not.to.be.undefined;
|
|
239
|
+
// @ts-expect-error [allow-protected-in-tests]
|
|
240
|
+
await dialogAfterRouteChange._overlayCtrl._showComplete;
|
|
241
|
+
expect(dialogAfterRouteChange.opened).to.be.true;
|
|
242
|
+
});
|
|
243
|
+
});
|
|
190
244
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { html, LitElement } from 'lit';
|
|
2
|
+
|
|
3
|
+
class TestRouter extends LitElement {
|
|
4
|
+
static properties = {
|
|
5
|
+
routingMap: {
|
|
6
|
+
type: Object,
|
|
7
|
+
attribute: false,
|
|
8
|
+
},
|
|
9
|
+
path: { type: String },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
|
|
15
|
+
/** @type {{ [path: string]: HTMLElement }} */
|
|
16
|
+
this.routingMap = {};
|
|
17
|
+
/** @type {string} */
|
|
18
|
+
this.path = '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render() {
|
|
22
|
+
return html`
|
|
23
|
+
<div>
|
|
24
|
+
<div id="selector">
|
|
25
|
+
${Object.keys(this.routingMap).map(
|
|
26
|
+
path =>
|
|
27
|
+
html`<button
|
|
28
|
+
id="path-${path}"
|
|
29
|
+
@click="${() => {
|
|
30
|
+
this.path = path;
|
|
31
|
+
}}"
|
|
32
|
+
>
|
|
33
|
+
${path}
|
|
34
|
+
</button>`,
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
<div id="view">${this.routingMap[this.path]}</div>
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
customElements.define('test-router', TestRouter);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/* eslint-disable class-methods-use-this */
|
|
2
2
|
|
|
3
3
|
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
4
|
+
|
|
5
|
+
import { ValidateMixin } from './validate/ValidateMixin.js';
|
|
4
6
|
import { FormControlMixin } from './FormControlMixin.js';
|
|
5
7
|
import { Unparseable } from './validate/Unparseable.js';
|
|
6
|
-
import { ValidateMixin } from './validate/ValidateMixin.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* @typedef {import('../types/FormatMixinTypes.js').FormatMixin} FormatMixin
|
|
10
|
-
* @typedef {import('../types/FormatMixinTypes.js').FormatOptions} FormatOptions
|
|
11
10
|
* @typedef {import('../types/FormControlMixinTypes.js').ModelValueEventDetails} ModelValueEventDetails
|
|
11
|
+
* @typedef {import('../types/FormatMixinTypes.js').FormatOptions} FormatOptions
|
|
12
|
+
* @typedef {import('../types/FormatMixinTypes.js').FormatMixin} FormatMixin
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
// For a future breaking release:
|
|
@@ -69,6 +70,16 @@ const FormatMixinImplementation = superclass =>
|
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* During format/parse, we sometimes need to know how the current viewValue the user
|
|
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.
|
|
77
|
+
*/
|
|
78
|
+
#viewValueState = {
|
|
79
|
+
didFormatterOutputSyncToView: false,
|
|
80
|
+
didFormatterRun: false,
|
|
81
|
+
};
|
|
82
|
+
|
|
72
83
|
/**
|
|
73
84
|
* @param {string} [name]
|
|
74
85
|
* @param {unknown} [oldValue]
|
|
@@ -93,7 +104,7 @@ const FormatMixinImplementation = superclass =>
|
|
|
93
104
|
* The view value. Will be delegated to `._inputNode.value`
|
|
94
105
|
*/
|
|
95
106
|
get value() {
|
|
96
|
-
return
|
|
107
|
+
return this._inputNode?.value || this.__value || '';
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
/** @param {string} value */
|
|
@@ -249,7 +260,11 @@ const FormatMixinImplementation = superclass =>
|
|
|
249
260
|
// Apparently, the parser was not able to produce a satisfactory output for the desired
|
|
250
261
|
// modelValue type, based on the current viewValue. Unparseable allows to restore all
|
|
251
262
|
// states (for instance from a lost user session), since it saves the current viewValue.
|
|
252
|
-
const result = this.parser(value,
|
|
263
|
+
const result = this.parser(value, {
|
|
264
|
+
...this.formatOptions,
|
|
265
|
+
mode: this.#getFormatOptionsMode(),
|
|
266
|
+
viewValueStates: this.#getFormatOptionsViewValueStates(),
|
|
267
|
+
});
|
|
253
268
|
return result !== undefined ? result : new Unparseable(value);
|
|
254
269
|
}
|
|
255
270
|
|
|
@@ -258,6 +273,8 @@ const FormatMixinImplementation = superclass =>
|
|
|
258
273
|
* @private
|
|
259
274
|
*/
|
|
260
275
|
_callFormatter() {
|
|
276
|
+
this.#viewValueState.didFormatterRun = false;
|
|
277
|
+
|
|
261
278
|
// - Why check for this.hasError?
|
|
262
279
|
// We only want to format values that are considered valid. For best UX,
|
|
263
280
|
// we only 'reward' valid inputs.
|
|
@@ -269,13 +286,8 @@ const FormatMixinImplementation = superclass =>
|
|
|
269
286
|
// imperatively, we DO want to format a value (it is the only way to get meaningful
|
|
270
287
|
// input into `._inputNode` with modelValue as input)
|
|
271
288
|
|
|
272
|
-
if (
|
|
273
|
-
this.
|
|
274
|
-
this.hasFeedbackFor?.length &&
|
|
275
|
-
this.hasFeedbackFor.includes('error') &&
|
|
276
|
-
this._inputNode
|
|
277
|
-
) {
|
|
278
|
-
return this._inputNode ? this.value : undefined;
|
|
289
|
+
if (this._isHandlingUserInput && this.hasFeedbackFor?.includes('error') && this._inputNode) {
|
|
290
|
+
return this.value;
|
|
279
291
|
}
|
|
280
292
|
|
|
281
293
|
if (this.modelValue instanceof Unparseable) {
|
|
@@ -285,7 +297,13 @@ const FormatMixinImplementation = superclass =>
|
|
|
285
297
|
return this.modelValue.viewValue;
|
|
286
298
|
}
|
|
287
299
|
|
|
288
|
-
|
|
300
|
+
this.#viewValueState.didFormatterRun = true;
|
|
301
|
+
|
|
302
|
+
return this.formatter(this.modelValue, {
|
|
303
|
+
...this.formatOptions,
|
|
304
|
+
mode: this.#getFormatOptionsMode(),
|
|
305
|
+
viewValueStates: this.#getFormatOptionsViewValueStates(),
|
|
306
|
+
});
|
|
289
307
|
}
|
|
290
308
|
|
|
291
309
|
/**
|
|
@@ -348,7 +366,6 @@ const FormatMixinImplementation = superclass =>
|
|
|
348
366
|
* @private
|
|
349
367
|
*/
|
|
350
368
|
__handlePreprocessor() {
|
|
351
|
-
const unprocessedValue = this.value;
|
|
352
369
|
let currentCaretIndex = this.value.length;
|
|
353
370
|
// Be gentle with Safari
|
|
354
371
|
if (
|
|
@@ -364,7 +381,6 @@ const FormatMixinImplementation = superclass =>
|
|
|
364
381
|
prevViewValue: this.__prevViewValue,
|
|
365
382
|
});
|
|
366
383
|
|
|
367
|
-
this.__prevViewValue = unprocessedValue;
|
|
368
384
|
if (preprocessedValue === undefined) {
|
|
369
385
|
// Make sure we do no set back original value, so we preserve
|
|
370
386
|
// caret index (== selectionStart/selectionEnd)
|
|
@@ -393,6 +409,8 @@ const FormatMixinImplementation = superclass =>
|
|
|
393
409
|
if (this._reflectBackOn()) {
|
|
394
410
|
// Text 'undefined' should not end up in <input>
|
|
395
411
|
this.value = typeof this.formattedValue !== 'undefined' ? this.formattedValue : '';
|
|
412
|
+
this.#viewValueState.didFormatterOutputSyncToView =
|
|
413
|
+
Boolean(this.formattedValue) && this.#viewValueState.didFormatterRun;
|
|
396
414
|
}
|
|
397
415
|
}
|
|
398
416
|
|
|
@@ -459,7 +477,7 @@ const FormatMixinImplementation = superclass =>
|
|
|
459
477
|
/**
|
|
460
478
|
* Configuration object that will be available inside the formatter function
|
|
461
479
|
*/
|
|
462
|
-
this.formatOptions = /** @type {FormatOptions} */ ({});
|
|
480
|
+
this.formatOptions = /** @type {FormatOptions} */ ({ mode: 'auto' });
|
|
463
481
|
|
|
464
482
|
/**
|
|
465
483
|
* The view value is the result of the formatter function (when available).
|
|
@@ -543,10 +561,8 @@ const FormatMixinImplementation = superclass =>
|
|
|
543
561
|
*/
|
|
544
562
|
__onPaste() {
|
|
545
563
|
this._isPasting = true;
|
|
546
|
-
this.formatOptions.mode = 'pasted';
|
|
547
564
|
setTimeout(() => {
|
|
548
565
|
this._isPasting = false;
|
|
549
|
-
this.formatOptions.mode = 'auto';
|
|
550
566
|
});
|
|
551
567
|
}
|
|
552
568
|
|
|
@@ -588,6 +604,38 @@ const FormatMixinImplementation = superclass =>
|
|
|
588
604
|
this._inputNode.removeEventListener('compositionend', this.__onCompositionEvent);
|
|
589
605
|
}
|
|
590
606
|
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* This method will be called right before a parser or formatter gets called and computes
|
|
610
|
+
* the formatOptions.mode. In short, it gives meta info about how the user interacted with
|
|
611
|
+
* the form control, as these user interactions can influence the formatting/parsing behavior.
|
|
612
|
+
*
|
|
613
|
+
* It's important that the behavior of these can be different during a paste or user edit,
|
|
614
|
+
* but not during programmatic setting of values (like setting modelValue).
|
|
615
|
+
* @returns {'auto'|'pasted'|'user-edited'}
|
|
616
|
+
*/
|
|
617
|
+
#getFormatOptionsMode() {
|
|
618
|
+
if (this._isPasting) {
|
|
619
|
+
return 'pasted';
|
|
620
|
+
}
|
|
621
|
+
const isUserEditing = this._isHandlingUserInput && this.__prevViewValue;
|
|
622
|
+
if (isUserEditing) {
|
|
623
|
+
return 'user-edited';
|
|
624
|
+
}
|
|
625
|
+
return 'auto';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @returns {FormatOptions['viewValueStates']}
|
|
630
|
+
*/
|
|
631
|
+
#getFormatOptionsViewValueStates() {
|
|
632
|
+
/** @type {FormatOptions['viewValueStates']}} */
|
|
633
|
+
const states = [];
|
|
634
|
+
if (this.#viewValueState.didFormatterOutputSyncToView) {
|
|
635
|
+
states.push('formatted');
|
|
636
|
+
}
|
|
637
|
+
return states;
|
|
638
|
+
}
|
|
591
639
|
};
|
|
592
640
|
|
|
593
641
|
export const FormatMixin = dedupeMixin(FormatMixinImplementation);
|
|
@@ -5,6 +5,8 @@ import sinon from 'sinon';
|
|
|
5
5
|
import { Unparseable, Validator, FormatMixin } from '@lion/ui/form-core.js';
|
|
6
6
|
import { getFormControlMembers, mimicUserInput } from '@lion/ui/form-core-test-helpers.js';
|
|
7
7
|
|
|
8
|
+
const isLionInputStepper = (/** @type {FormatClass} */ el) => 'valueTextMapping' in el;
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @typedef {import('../types/FormControlMixinTypes.js').FormControlHost} FormControlHost
|
|
10
12
|
* @typedef {ArrayConstructor | ObjectConstructor | NumberConstructor | BooleanConstructor | StringConstructor | DateConstructor | 'iban' | 'email'} modelValueType
|
|
@@ -480,7 +482,7 @@ export function runFormatMixinSuite(customConfig) {
|
|
|
480
482
|
/**
|
|
481
483
|
* @param {FormatClass} el
|
|
482
484
|
*/
|
|
483
|
-
function
|
|
485
|
+
function mimicPaste(el, val = 'lorem') {
|
|
484
486
|
const { _inputNode } = getFormControlMembers(el);
|
|
485
487
|
_inputNode.value = val;
|
|
486
488
|
_inputNode.dispatchEvent(new ClipboardEvent('paste', { bubbles: true }));
|
|
@@ -494,12 +496,17 @@ export function runFormatMixinSuite(customConfig) {
|
|
|
494
496
|
`)
|
|
495
497
|
);
|
|
496
498
|
const formatterSpy = sinon.spy(el, 'formatter');
|
|
497
|
-
|
|
499
|
+
mimicPaste(el);
|
|
498
500
|
expect(formatterSpy).to.be.called;
|
|
499
|
-
expect(/** @type {{mode: string}} */ (formatterSpy.args[
|
|
501
|
+
expect(/** @type {{mode: string}} */ (formatterSpy.lastCall.args[1]).mode).to.equal(
|
|
502
|
+
'pasted',
|
|
503
|
+
);
|
|
504
|
+
// give microtask of _isPasting chance to reset
|
|
500
505
|
await aTimeout(0);
|
|
501
|
-
|
|
502
|
-
expect(/** @type {{mode: string}} */ (formatterSpy.args[
|
|
506
|
+
el.modelValue = 'foo';
|
|
507
|
+
expect(/** @type {{mode: string}} */ (formatterSpy.lastCall.args[1]).mode).to.equal(
|
|
508
|
+
'auto',
|
|
509
|
+
);
|
|
503
510
|
});
|
|
504
511
|
|
|
505
512
|
it('sets protected value "_isPasting" for Subclassers', async () => {
|
|
@@ -509,7 +516,7 @@ export function runFormatMixinSuite(customConfig) {
|
|
|
509
516
|
`)
|
|
510
517
|
);
|
|
511
518
|
const formatterSpy = sinon.spy(el, 'formatter');
|
|
512
|
-
|
|
519
|
+
mimicPaste(el);
|
|
513
520
|
expect(formatterSpy).to.have.been.called;
|
|
514
521
|
// @ts-ignore [allow-protected] in test
|
|
515
522
|
expect(el._isPasting).to.be.true;
|
|
@@ -526,7 +533,7 @@ export function runFormatMixinSuite(customConfig) {
|
|
|
526
533
|
);
|
|
527
534
|
// @ts-ignore [allow-protected] in test
|
|
528
535
|
const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
|
|
529
|
-
|
|
536
|
+
mimicPaste(el);
|
|
530
537
|
expect(reflectBackSpy).to.have.been.called;
|
|
531
538
|
});
|
|
532
539
|
|
|
@@ -538,10 +545,65 @@ export function runFormatMixinSuite(customConfig) {
|
|
|
538
545
|
);
|
|
539
546
|
// @ts-ignore [allow-protected] in test
|
|
540
547
|
const reflectBackSpy = sinon.spy(el, '_reflectBackOn');
|
|
541
|
-
|
|
548
|
+
mimicPaste(el);
|
|
542
549
|
expect(reflectBackSpy).to.have.been.called;
|
|
543
550
|
});
|
|
544
551
|
});
|
|
552
|
+
|
|
553
|
+
describe('On user input', () => {
|
|
554
|
+
it('adjusts formatOptions.mode to "user-edited" for parser when user changes value', async () => {
|
|
555
|
+
const el = /** @type {FormatClass} */ (
|
|
556
|
+
await fixture(html`<${tag}><input slot="input"></${tag}>`)
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const parserSpy = sinon.spy(el, 'parser');
|
|
560
|
+
|
|
561
|
+
// Here we get auto as we start from '' (so there was no value to edit)
|
|
562
|
+
mimicUserInput(el, 'some val');
|
|
563
|
+
expect(/** @type {{mode: string}} */ (parserSpy.lastCall.args[1]).mode).to.equal(
|
|
564
|
+
'auto',
|
|
565
|
+
);
|
|
566
|
+
await el.updateComplete;
|
|
567
|
+
|
|
568
|
+
mimicUserInput(el, 'some other val');
|
|
569
|
+
expect(/** @type {{mode: string}} */ (parserSpy.lastCall.args[1]).mode).to.equal(
|
|
570
|
+
'user-edited',
|
|
571
|
+
);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('adjusts formatOptions.viewValueStates when user already formatted value', async () => {
|
|
575
|
+
const el = /** @type {FormatClass} */ (
|
|
576
|
+
await fixture(html`<${tag}><input slot="input"></${tag}>`)
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Neutralize config, so that we know for sure that the user has formatted the value
|
|
580
|
+
// when this suite is run on extended classes
|
|
581
|
+
|
|
582
|
+
// always reflect back formattedVlaue to view (instead of on blur or else)
|
|
583
|
+
// @ts-expect-error [allow-protected] in test
|
|
584
|
+
el._reflectBackOn = () => true;
|
|
585
|
+
// avoid non-formatted values due to validators in extended classes
|
|
586
|
+
el.defaultValidators = [];
|
|
587
|
+
// avoid Unparseable due to parsers in extended classes
|
|
588
|
+
el.parser = v => v;
|
|
589
|
+
// @ts-expect-error
|
|
590
|
+
el.formatter = v => v;
|
|
591
|
+
|
|
592
|
+
const parserSpy = sinon.spy(el, 'parser');
|
|
593
|
+
const formatterSpy = sinon.spy(el, 'formatter');
|
|
594
|
+
|
|
595
|
+
mimicUserInput(el, 'some val');
|
|
596
|
+
expect(parserSpy.lastCall.args[1].viewValueStates).to.not.include('formatted');
|
|
597
|
+
// @ts-expect-error
|
|
598
|
+
expect(formatterSpy.lastCall.args[1].viewValueStates).to.not.include('formatted');
|
|
599
|
+
await el.updateComplete;
|
|
600
|
+
|
|
601
|
+
mimicUserInput(el, 'some other val');
|
|
602
|
+
expect(parserSpy.lastCall.args[1].viewValueStates).to.include('formatted');
|
|
603
|
+
// @ts-expect-error
|
|
604
|
+
expect(formatterSpy.lastCall.args[1].viewValueStates).to.include('formatted');
|
|
605
|
+
});
|
|
606
|
+
});
|
|
545
607
|
});
|
|
546
608
|
|
|
547
609
|
describe('Parser', () => {
|
|
@@ -597,6 +659,43 @@ export function runFormatMixinSuite(customConfig) {
|
|
|
597
659
|
expect(_inputNode.value).to.equal(val);
|
|
598
660
|
});
|
|
599
661
|
|
|
662
|
+
it('does only calculate derived values as consequence of user input when preprocessed value is different from previous view value', async () => {
|
|
663
|
+
const val = generateValueBasedOnType({ viewValue: true }) || 'init-value';
|
|
664
|
+
if (typeof val !== 'string') return;
|
|
665
|
+
|
|
666
|
+
const preprocessorSpy = sinon.spy(v => v.replace(/\$$/g, ''));
|
|
667
|
+
const el = /** @type {FormatClass} */ (
|
|
668
|
+
await fixture(html`
|
|
669
|
+
<${tag} .preprocessor=${preprocessorSpy}>
|
|
670
|
+
<input slot="input" .value="${val}">
|
|
671
|
+
</${tag}>
|
|
672
|
+
`)
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
// TODO: find out why we need to skip this for lion-input-stepper
|
|
676
|
+
if (isLionInputStepper(el)) return;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* The _calculateValues method is called inside _onUserInputChanged w/o providing args
|
|
680
|
+
* @param {sinon.SinonSpyCall} call
|
|
681
|
+
* @returns {boolean}
|
|
682
|
+
*/
|
|
683
|
+
const isCalculateCallAfterUserInput = call => call.args[0]?.length === 0;
|
|
684
|
+
|
|
685
|
+
const didRecalculateAfterUserInput = (/** @type {sinon.SinonSpy<any[], any>} */ spy) =>
|
|
686
|
+
spy.callCount > 1 && !spy.getCalls().find(isCalculateCallAfterUserInput);
|
|
687
|
+
|
|
688
|
+
// @ts-expect-error [allow-protected] in test
|
|
689
|
+
const calcValuesSpy = sinon.spy(el, '_calculateValues');
|
|
690
|
+
// this value gets preprocessed to 'val'
|
|
691
|
+
mimicUserInput(el, `${val}$`);
|
|
692
|
+
expect(didRecalculateAfterUserInput(calcValuesSpy)).to.be.false;
|
|
693
|
+
|
|
694
|
+
// this value gets preprocessed to 'value' (and thus differs from previous)
|
|
695
|
+
mimicUserInput(el, `${val}ue$`);
|
|
696
|
+
expect(didRecalculateAfterUserInput(calcValuesSpy)).to.be.true;
|
|
697
|
+
});
|
|
698
|
+
|
|
600
699
|
it('does not preprocess during composition', async () => {
|
|
601
700
|
const el = /** @type {FormatClass} */ (
|
|
602
701
|
await fixture(html`
|
|
@@ -3,7 +3,10 @@ import { LitElement } from 'lit';
|
|
|
3
3
|
import { ValidateHost } from './validate/ValidateMixinTypes.js';
|
|
4
4
|
import { FormControlHost } from './FormControlMixinTypes.js';
|
|
5
5
|
|
|
6
|
-
export type FormatOptions = {
|
|
6
|
+
export type FormatOptions = {
|
|
7
|
+
mode: 'pasted' | 'auto' | 'user-edited';
|
|
8
|
+
viewValueStates?: 'formatted'[];
|
|
9
|
+
};
|
|
7
10
|
export declare class FormatHost {
|
|
8
11
|
/**
|
|
9
12
|
* Converts viewValue to modelValue
|
|
@@ -9,6 +9,7 @@ import { html } from 'lit';
|
|
|
9
9
|
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
|
10
10
|
import { getAllTagNames } from './helpers/helpers.js';
|
|
11
11
|
import './helpers/umbrella-form.js';
|
|
12
|
+
|
|
12
13
|
/**
|
|
13
14
|
* @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm
|
|
14
15
|
* @typedef {import('../../dialog/src/LionDialog.js').LionDialog} LionDialog
|
|
@@ -25,10 +26,12 @@ describe('Form inside dialog Integrations', () => {
|
|
|
25
26
|
</lion-dialog>`,
|
|
26
27
|
);
|
|
27
28
|
|
|
28
|
-
// @ts-
|
|
29
|
-
const
|
|
29
|
+
// @ts-expect-error [allow-protected-in-tests]
|
|
30
|
+
const umbrellaEl = el._overlayCtrl.contentNode;
|
|
31
|
+
const formEl = /** @type {LionForm} */ (umbrellaEl._lionFormNode);
|
|
30
32
|
await formEl.registrationComplete;
|
|
31
33
|
const registeredEls = getAllTagNames(formEl);
|
|
34
|
+
await umbrellaEl.waitForAllChildrenUpdates();
|
|
32
35
|
|
|
33
36
|
expect(registeredEls).to.eql([
|
|
34
37
|
'lion-fieldset',
|
|
@@ -86,6 +89,7 @@ describe('Form inside dialog Integrations', () => {
|
|
|
86
89
|
</div>
|
|
87
90
|
</lion-dialog>
|
|
88
91
|
`);
|
|
92
|
+
|
|
89
93
|
// @ts-expect-error [allow-protected-in-tests]
|
|
90
94
|
el._overlayInvokerNode.click();
|
|
91
95
|
const lionInput = el.querySelector('[name="input"]');
|
|
@@ -48,7 +48,7 @@ describe('Form Integrations', () => {
|
|
|
48
48
|
notifications: { value: '', checked: false },
|
|
49
49
|
rsvp: '',
|
|
50
50
|
tel: '',
|
|
51
|
-
'tel-dropdown': '
|
|
51
|
+
'tel-dropdown': '',
|
|
52
52
|
comments: '',
|
|
53
53
|
});
|
|
54
54
|
});
|
|
@@ -82,7 +82,7 @@ describe('Form Integrations', () => {
|
|
|
82
82
|
notifications: '',
|
|
83
83
|
rsvp: '',
|
|
84
84
|
tel: '',
|
|
85
|
-
'tel-dropdown': '
|
|
85
|
+
'tel-dropdown': '',
|
|
86
86
|
comments: '',
|
|
87
87
|
});
|
|
88
88
|
});
|