@lion/ui 0.12.0 → 0.13.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 +26 -8
- package/components/combobox/test/lion-combobox.test.js +206 -57
- package/components/form-core/src/FocusMixin.js +1 -0
- package/components/form-core/src/FormControlMixin.js +23 -18
- package/components/form-core/src/validate/ValidateMixin.js +17 -1
- package/components/form-core/test/FormControlMixin.test.js +68 -0
- package/components/form-core/test-suites/ValidateMixin.suite.js +19 -3
- package/components/overlays/src/OverlayController.js +32 -5
- package/components/overlays/src/OverlayMixin.js +1 -0
- package/components/overlays/src/configurations/withDropdownConfig.js +0 -1
- package/components/overlays/src/configurations/withTooltipConfig.js +0 -1
- package/components/overlays/test/OverlayController.test.js +16 -4
- package/components/overlays/test-suites/OverlayMixin.suite.js +71 -0
- package/custom-elements.json +1144 -327
- package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
- package/dist-types/components/core/src/ScopedElementsMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/src/FocusMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/src/FormControlMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/src/validate/ValidateMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/ValidateMixin.suite.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/src/configurations/withDropdownConfig.d.ts.map +1 -1
- package/dist-types/components/overlays/src/configurations/withTooltipConfig.d.ts.map +1 -1
- package/dist-types/components/overlays/test/OverlayController.test.d.ts.map +1 -1
- package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/api-table.md +6 -6
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/api-table.md +2 -2
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +90 -1
- package/docs/components/dialog/overview.md +1 -1
- package/docs/components/fieldset/api-table.md +2 -2
- 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 -2
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/api-table.md +2 -2
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-date/api-table.md +2 -2
- package/docs/components/input-date/overview.md +3 -3
- package/docs/components/input-datepicker/api-table.md +2 -2
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/api-table.md +2 -2
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/api-table.md +2 -2
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-iban/api-table.md +2 -2
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/api-table.md +2 -2
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-stepper/api-table.md +2 -2
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-tel/api-table.md +2 -2
- 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 -2
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/api-table.md +2 -2
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/radio-group/api-table.md +4 -4
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/api-table.md +2 -2
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/api-table.md +2 -2
- package/docs/components/select-rich/use-cases.md +1 -1
- package/docs/components/switch/api-table.md +2 -2
- package/docs/components/tabs/examples.md +1 -1
- package/docs/components/textarea/api-table.md +2 -2
- 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 -2
- 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/styling.md +3 -20
- 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 +3 -3
|
@@ -502,6 +502,10 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|
|
502
502
|
if (this._selectionDisplayNode) {
|
|
503
503
|
this._selectionDisplayNode.comboboxElement = this;
|
|
504
504
|
}
|
|
505
|
+
|
|
506
|
+
if (this.disabled || this.readOnly) {
|
|
507
|
+
this.__setComboboxDisabledAndReadOnly();
|
|
508
|
+
}
|
|
505
509
|
}
|
|
506
510
|
|
|
507
511
|
/**
|
|
@@ -657,9 +661,11 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|
|
657
661
|
const alwaysHideOn = ['Tab', 'Escape'];
|
|
658
662
|
const notMultipleChoiceHideOn = ['Enter'];
|
|
659
663
|
if (
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
664
|
+
this.disabled ||
|
|
665
|
+
this.readOnly ||
|
|
666
|
+
(lastKey &&
|
|
667
|
+
(alwaysHideOn.includes(lastKey) ||
|
|
668
|
+
(!this.multipleChoice && notMultipleChoiceHideOn.includes(lastKey))))
|
|
663
669
|
) {
|
|
664
670
|
return false;
|
|
665
671
|
}
|
|
@@ -1174,9 +1180,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|
|
1174
1180
|
*/
|
|
1175
1181
|
// eslint-disable-next-line no-unused-vars
|
|
1176
1182
|
_syncToTextboxCondition(modelValue, oldModelValue, { phase } = {}) {
|
|
1177
|
-
return
|
|
1178
|
-
this.autocomplete === 'inline' || this.autocomplete === 'both' || phase === 'overlay-close'
|
|
1179
|
-
);
|
|
1183
|
+
return this.autocomplete === 'both' || this.autocomplete === 'inline' || !this.focused;
|
|
1180
1184
|
}
|
|
1181
1185
|
|
|
1182
1186
|
/**
|
|
@@ -1213,8 +1217,22 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
|
|
|
1213
1217
|
*/
|
|
1214
1218
|
__setComboboxDisabledAndReadOnly() {
|
|
1215
1219
|
if (this._comboboxNode) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1220
|
+
// Since `._comboboxNode` can either be `<div "role=combobox">` or `<input>`,
|
|
1221
|
+
// we need to cater for both scenarios (aria for semantics, "native attr" for operability)
|
|
1222
|
+
this._comboboxNode.toggleAttribute('disabled', this.disabled);
|
|
1223
|
+
this._comboboxNode.setAttribute('aria-disabled', `${this.disabled}`);
|
|
1224
|
+
this._comboboxNode.toggleAttribute('readonly', this.readOnly);
|
|
1225
|
+
this._comboboxNode.setAttribute('aria-readonly', `${this.readOnly}`);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (this._inputNode) {
|
|
1229
|
+
// N.B. in case ._inputNode === ._comboboxNode (we have <input role="combobox">)
|
|
1230
|
+
// this value has already been set above. This is fine, as a toggle with boolean flag is idempotent.
|
|
1231
|
+
this._inputNode.toggleAttribute('disabled', this.disabled);
|
|
1232
|
+
this._inputNode.toggleAttribute('readOnly', this.readOnly);
|
|
1233
|
+
|
|
1234
|
+
this._inputNode.setAttribute('aria-readonly', `${this.readOnly}`);
|
|
1235
|
+
this._inputNode.tabIndex = this.disabled ? -1 : 0;
|
|
1218
1236
|
}
|
|
1219
1237
|
}
|
|
1220
1238
|
|
|
@@ -24,12 +24,12 @@ import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* @param {{ autocomplete?:'none'|'list'|'both', matchMode?:'begin'|'all' }} config
|
|
27
|
+
* @param {{ autocomplete?:'none'|'list'|'both', matchMode?:'begin'|'all', disabled?: boolean, readonly?: boolean }} config
|
|
28
28
|
*/
|
|
29
|
-
async function fruitFixture({ autocomplete, matchMode } = {}) {
|
|
29
|
+
async function fruitFixture({ autocomplete, matchMode, disabled, readonly } = {}) {
|
|
30
30
|
const el = /** @type {LionCombobox} */ (
|
|
31
31
|
await fixture(html`
|
|
32
|
-
<lion-combobox name="foo">
|
|
32
|
+
<lion-combobox label="Search" name="foo">
|
|
33
33
|
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
|
34
34
|
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
|
35
35
|
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
|
@@ -43,6 +43,12 @@ async function fruitFixture({ autocomplete, matchMode } = {}) {
|
|
|
43
43
|
if (matchMode) {
|
|
44
44
|
el.matchMode = matchMode;
|
|
45
45
|
}
|
|
46
|
+
if (disabled) {
|
|
47
|
+
el.disabled = disabled;
|
|
48
|
+
}
|
|
49
|
+
if (readonly) {
|
|
50
|
+
el.readOnly = readonly;
|
|
51
|
+
}
|
|
46
52
|
await el.updateComplete;
|
|
47
53
|
return [el, el.formElements];
|
|
48
54
|
}
|
|
@@ -634,38 +640,39 @@ describe('lion-combobox', () => {
|
|
|
634
640
|
});
|
|
635
641
|
|
|
636
642
|
it('syncs textbox to modelValue', async () => {
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
<lion-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
643
|
+
for (const autocompleteMode of ['none', 'list', 'inline', 'both']) {
|
|
644
|
+
const el = /** @type {LionCombobox} */ (
|
|
645
|
+
await fixture(html`
|
|
646
|
+
<lion-combobox name="foo" show-all-on-empty .autocomplete="${autocompleteMode}">
|
|
647
|
+
<lion-option .choiceValue="${'Aa'}" checked>Aa</lion-option>
|
|
648
|
+
<lion-option .choiceValue="${'Bb'}">Bb</lion-option>
|
|
649
|
+
</lion-combobox>
|
|
650
|
+
`)
|
|
651
|
+
);
|
|
652
|
+
const { _inputNode } = getComboboxMembers(el);
|
|
653
|
+
|
|
654
|
+
expect(_inputNode.value).to.equal('Aa', `autocompleteMode is ${autocompleteMode}`);
|
|
646
655
|
|
|
647
|
-
/** @param {string} autocompleteMode */
|
|
648
|
-
async function performChecks(autocompleteMode) {
|
|
649
656
|
el.formElements[0].click();
|
|
650
657
|
await el.updateComplete;
|
|
651
658
|
|
|
652
|
-
|
|
653
|
-
// expect(_inputNode.value).to.equal('Aha', `autocompleteMode is ${autocompleteMode}`);
|
|
659
|
+
expect(_inputNode.value).to.equal('Aa', `autocompleteMode is ${autocompleteMode}`);
|
|
654
660
|
expect(el.checkedIndex).to.equal(0, `autocompleteMode is ${autocompleteMode}`);
|
|
655
661
|
|
|
656
|
-
await mimicUserTyping(el, '
|
|
662
|
+
await mimicUserTyping(el, 'B');
|
|
657
663
|
await el.updateComplete;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
664
|
+
if (autocompleteMode === 'none' || autocompleteMode === 'list') {
|
|
665
|
+
expect(el.modelValue).to.deep.equal(
|
|
666
|
+
{ type: 'unparseable', viewValue: 'B' },
|
|
667
|
+
`autocompleteMode is ${autocompleteMode}`,
|
|
668
|
+
);
|
|
669
|
+
await el.updateComplete;
|
|
670
|
+
expect(el.checkedIndex).to.equal(-1, `autocompleteMode is ${autocompleteMode}`);
|
|
671
|
+
} else {
|
|
672
|
+
expect(el.modelValue).to.equal('Bb', `autocompleteMode is ${autocompleteMode}`);
|
|
673
|
+
expect(el.checkedIndex).to.equal(1, `autocompleteMode is ${autocompleteMode}`);
|
|
674
|
+
}
|
|
662
675
|
}
|
|
663
|
-
|
|
664
|
-
el.autocomplete = 'none';
|
|
665
|
-
await performChecks('none');
|
|
666
|
-
|
|
667
|
-
el.autocomplete = 'list';
|
|
668
|
-
await performChecks('list');
|
|
669
676
|
});
|
|
670
677
|
|
|
671
678
|
it('works with Required validator', async () => {
|
|
@@ -986,6 +993,27 @@ describe('lion-combobox', () => {
|
|
|
986
993
|
expect(el.modelValue).to.eql(['Art']);
|
|
987
994
|
});
|
|
988
995
|
|
|
996
|
+
it('allows prefilling the combobox', async () => {
|
|
997
|
+
const autocompleteValues = ['inline', 'none', 'both', 'list'];
|
|
998
|
+
|
|
999
|
+
for (const autocompleteValue of autocompleteValues) {
|
|
1000
|
+
const el = /** @type {LionCombobox} */ (
|
|
1001
|
+
await fixture(html`
|
|
1002
|
+
<lion-combobox autocomplete="${autocompleteValue}" .modelValue="${'Chard'}" name="foo">
|
|
1003
|
+
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
|
1004
|
+
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
|
1005
|
+
<lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
|
|
1006
|
+
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
|
1007
|
+
</lion-combobox>
|
|
1008
|
+
`)
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
expect(el.modelValue).to.equal('Chard');
|
|
1012
|
+
// @ts-expect-error
|
|
1013
|
+
expect(el._inputNode.value).to.equal('Chard');
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
989
1017
|
it('submits form on [Enter] when listbox is closed', async () => {
|
|
990
1018
|
const submitSpy = sinon.spy(e => e.preventDefault());
|
|
991
1019
|
const el = /** @type {HTMLFormElement} */ (
|
|
@@ -2312,8 +2340,7 @@ describe('lion-combobox', () => {
|
|
|
2312
2340
|
</${tag}>
|
|
2313
2341
|
`)
|
|
2314
2342
|
);
|
|
2315
|
-
|
|
2316
|
-
el.autocomplete = 'list';
|
|
2343
|
+
|
|
2317
2344
|
await mimicUserTypingAdvanced(el, ['v', 'i']); // so we have options ['Victoria Plum']
|
|
2318
2345
|
await el.updateComplete;
|
|
2319
2346
|
expect(el.checkedIndex).to.equal(3);
|
|
@@ -2440,23 +2467,26 @@ describe('lion-combobox', () => {
|
|
|
2440
2467
|
expect(_inputNode.value).to.equal('');
|
|
2441
2468
|
|
|
2442
2469
|
el.setCheckedIndex(-1);
|
|
2443
|
-
el.autocomplete = '
|
|
2444
|
-
el
|
|
2445
|
-
expect(_inputNode.value).to.equal('');
|
|
2470
|
+
el.autocomplete = 'list';
|
|
2471
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2472
|
+
expect(_inputNode.value).to.equal('art');
|
|
2446
2473
|
|
|
2447
2474
|
el.setCheckedIndex(-1);
|
|
2448
|
-
el.
|
|
2449
|
-
el.
|
|
2450
|
-
|
|
2475
|
+
el.value = '';
|
|
2476
|
+
el.autocomplete = 'none';
|
|
2477
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2478
|
+
expect(_inputNode.value).to.equal('art');
|
|
2451
2479
|
|
|
2452
2480
|
el.setCheckedIndex(-1);
|
|
2481
|
+
el.value = '';
|
|
2453
2482
|
el.autocomplete = 'inline';
|
|
2454
|
-
el
|
|
2483
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2455
2484
|
expect(_inputNode.value).to.equal('Artichoke');
|
|
2456
2485
|
|
|
2457
2486
|
el.setCheckedIndex(-1);
|
|
2487
|
+
el.value = '';
|
|
2458
2488
|
el.autocomplete = 'both';
|
|
2459
|
-
el
|
|
2489
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2460
2490
|
expect(_inputNode.value).to.equal('Artichoke');
|
|
2461
2491
|
});
|
|
2462
2492
|
|
|
@@ -2476,30 +2506,28 @@ describe('lion-combobox', () => {
|
|
|
2476
2506
|
expect(_inputNode.value).to.eql('');
|
|
2477
2507
|
|
|
2478
2508
|
el.setCheckedIndex(-1);
|
|
2479
|
-
el.autocomplete = 'none';
|
|
2480
|
-
el.setCheckedIndex([0]);
|
|
2481
|
-
el.setCheckedIndex([1]);
|
|
2482
|
-
expect(_inputNode.value).to.equal('');
|
|
2483
|
-
|
|
2484
2509
|
el.setCheckedIndex(-1);
|
|
2485
2510
|
el.autocomplete = 'list';
|
|
2486
|
-
el
|
|
2487
|
-
|
|
2488
|
-
|
|
2511
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2512
|
+
expect(_inputNode.value).to.equal('art');
|
|
2513
|
+
|
|
2514
|
+
el.setCheckedIndex(-1);
|
|
2515
|
+
el.value = '';
|
|
2516
|
+
el.autocomplete = 'none';
|
|
2517
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2518
|
+
expect(_inputNode.value).to.equal('art');
|
|
2489
2519
|
|
|
2490
2520
|
el.setCheckedIndex(-1);
|
|
2521
|
+
el.value = '';
|
|
2491
2522
|
el.autocomplete = 'inline';
|
|
2492
|
-
el
|
|
2523
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2493
2524
|
expect(_inputNode.value).to.equal('Artichoke');
|
|
2494
|
-
el.setCheckedIndex([1]);
|
|
2495
|
-
expect(_inputNode.value).to.equal('Chard');
|
|
2496
2525
|
|
|
2497
2526
|
el.setCheckedIndex(-1);
|
|
2527
|
+
el.value = '';
|
|
2498
2528
|
el.autocomplete = 'both';
|
|
2499
|
-
el
|
|
2529
|
+
await mimicUserTypingAdvanced(el, ['a', 'r', 't']);
|
|
2500
2530
|
expect(_inputNode.value).to.equal('Artichoke');
|
|
2501
|
-
el.setCheckedIndex([1]);
|
|
2502
|
-
expect(_inputNode.value).to.equal('Chard');
|
|
2503
2531
|
});
|
|
2504
2532
|
|
|
2505
2533
|
describe('Subclassers', () => {
|
|
@@ -2748,12 +2776,6 @@ describe('lion-combobox', () => {
|
|
|
2748
2776
|
async function performChecks(autocompleteMode) {
|
|
2749
2777
|
await el.updateComplete;
|
|
2750
2778
|
|
|
2751
|
-
el.formElements[0].click();
|
|
2752
|
-
|
|
2753
|
-
// FIXME: fix properly for Webkit
|
|
2754
|
-
// expect(_inputNode.value).to.equal('Aha', autocompleteMode);
|
|
2755
|
-
expect(el.checkedIndex).to.equal(0, autocompleteMode);
|
|
2756
|
-
|
|
2757
2779
|
await mimicUserTypingAdvanced(el, ['A', 'r', 't', 'i']);
|
|
2758
2780
|
await el.updateComplete;
|
|
2759
2781
|
expect(_inputNode.value).to.equal('Arti', `autocompleteMode is ${autocompleteMode}`);
|
|
@@ -3431,4 +3453,131 @@ describe('lion-combobox', () => {
|
|
|
3431
3453
|
});
|
|
3432
3454
|
});
|
|
3433
3455
|
});
|
|
3456
|
+
|
|
3457
|
+
describe('Disabled state', () => {
|
|
3458
|
+
it('does not open overlay or allow input when disabled', async () => {
|
|
3459
|
+
const [el] = await fruitFixture({ disabled: true });
|
|
3460
|
+
expect(el.disabled).to.equal(true);
|
|
3461
|
+
|
|
3462
|
+
const { _inputNode } = getComboboxMembers(/** @type {LionCombobox} */ (el));
|
|
3463
|
+
|
|
3464
|
+
expect(el.disabled).to.be.true;
|
|
3465
|
+
expect(_inputNode.disabled).to.be.true;
|
|
3466
|
+
|
|
3467
|
+
// Try to open overlay by clicking input
|
|
3468
|
+
_inputNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
|
3469
|
+
await el.updateComplete;
|
|
3470
|
+
expect(el.opened).to.be.false;
|
|
3471
|
+
});
|
|
3472
|
+
|
|
3473
|
+
it('does open overlay or allow input when disabled state is removed after it was previously disabled', async () => {
|
|
3474
|
+
const [el] = await fruitFixture({ disabled: true });
|
|
3475
|
+
expect(el.disabled).to.equal(true);
|
|
3476
|
+
|
|
3477
|
+
const { _inputNode } = getComboboxMembers(/** @type {LionCombobox} */ (el));
|
|
3478
|
+
|
|
3479
|
+
expect(el.disabled).to.be.true;
|
|
3480
|
+
expect(_inputNode.disabled).to.be.true;
|
|
3481
|
+
|
|
3482
|
+
// Try to open overlay by clicking input
|
|
3483
|
+
_inputNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
|
3484
|
+
await el.updateComplete;
|
|
3485
|
+
expect(el.opened).to.be.false;
|
|
3486
|
+
|
|
3487
|
+
el.disabled = false;
|
|
3488
|
+
await el.updateComplete;
|
|
3489
|
+
|
|
3490
|
+
expect(el.disabled).to.be.false;
|
|
3491
|
+
expect(_inputNode.disabled).to.be.false;
|
|
3492
|
+
|
|
3493
|
+
// Try to open overlay by clicking input
|
|
3494
|
+
async function open() {
|
|
3495
|
+
await mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
|
3496
|
+
return el.updateComplete;
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
await open();
|
|
3500
|
+
expect(el.opened).to.be.true;
|
|
3501
|
+
});
|
|
3502
|
+
|
|
3503
|
+
it('sets aria-disabled and disables focus when combobox is disabled', async () => {
|
|
3504
|
+
const [el] = await fruitFixture({ disabled: true });
|
|
3505
|
+
const { _comboboxNode, _inputNode } = getComboboxMembers(/** @type {LionCombobox} */ (el));
|
|
3506
|
+
|
|
3507
|
+
// input should not be focusable
|
|
3508
|
+
expect(_comboboxNode.tabIndex).to.equal(-1);
|
|
3509
|
+
expect(_inputNode.tabIndex).to.equal(-1);
|
|
3510
|
+
|
|
3511
|
+
// aria-disabled should be set
|
|
3512
|
+
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
|
3513
|
+
|
|
3514
|
+
// aria-disabled should be set
|
|
3515
|
+
expect(_inputNode.getAttribute('aria-disabled')).to.equal('true');
|
|
3516
|
+
|
|
3517
|
+
await expect(el).to.be.accessible();
|
|
3518
|
+
});
|
|
3519
|
+
|
|
3520
|
+
it('ensure focus when combobox is readonly', async () => {
|
|
3521
|
+
const [el] = await fruitFixture({ readonly: true });
|
|
3522
|
+
const { _comboboxNode, _inputNode } = getComboboxMembers(/** @type {LionCombobox} */ (el));
|
|
3523
|
+
|
|
3524
|
+
// input should be focusable
|
|
3525
|
+
expect(_inputNode.tabIndex).to.equal(0);
|
|
3526
|
+
|
|
3527
|
+
// aria-disabled should be set
|
|
3528
|
+
expect(_comboboxNode.getAttribute('aria-readonly')).to.equal('true');
|
|
3529
|
+
|
|
3530
|
+
// aria-disabled should be set
|
|
3531
|
+
expect(_inputNode.getAttribute('aria-readonly')).to.equal('true');
|
|
3532
|
+
|
|
3533
|
+
await expect(el).to.be.accessible();
|
|
3534
|
+
});
|
|
3535
|
+
});
|
|
3536
|
+
|
|
3537
|
+
describe('Readonly state', () => {
|
|
3538
|
+
it('does not open overlay or allow input when readonly', async () => {
|
|
3539
|
+
const [el] = await fruitFixture({ readonly: true });
|
|
3540
|
+
|
|
3541
|
+
const { _inputNode } = getComboboxMembers(/** @type {LionCombobox} */ (el));
|
|
3542
|
+
expect(el.readOnly).to.equal(true);
|
|
3543
|
+
|
|
3544
|
+
expect(el.readOnly).to.be.true;
|
|
3545
|
+
expect(_inputNode.readOnly).to.be.true;
|
|
3546
|
+
|
|
3547
|
+
// Try to open overlay by clicking input
|
|
3548
|
+
_inputNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
|
3549
|
+
await el.updateComplete;
|
|
3550
|
+
expect(el.opened).to.be.false;
|
|
3551
|
+
});
|
|
3552
|
+
|
|
3553
|
+
it('does open overlay or allow input when readonly state is removed after it was previously set', async () => {
|
|
3554
|
+
const [el] = await fruitFixture({ readonly: true });
|
|
3555
|
+
expect(el.readOnly).to.equal(true);
|
|
3556
|
+
|
|
3557
|
+
const { _inputNode } = getComboboxMembers(/** @type {LionCombobox} */ (el));
|
|
3558
|
+
|
|
3559
|
+
expect(el.readOnly).to.be.true;
|
|
3560
|
+
expect(_inputNode.readOnly).to.be.true;
|
|
3561
|
+
|
|
3562
|
+
// Try to open overlay by clicking input
|
|
3563
|
+
_inputNode.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
|
|
3564
|
+
await el.updateComplete;
|
|
3565
|
+
expect(el.opened).to.be.false;
|
|
3566
|
+
|
|
3567
|
+
el.readOnly = false;
|
|
3568
|
+
await el.updateComplete;
|
|
3569
|
+
|
|
3570
|
+
expect(el.readOnly).to.be.false;
|
|
3571
|
+
expect(_inputNode.readOnly).to.be.false;
|
|
3572
|
+
|
|
3573
|
+
// Try to open overlay by clicking input
|
|
3574
|
+
async function open() {
|
|
3575
|
+
await mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
|
3576
|
+
return el.updateComplete;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
await open();
|
|
3580
|
+
expect(el.opened).to.be.true;
|
|
3581
|
+
});
|
|
3582
|
+
});
|
|
3434
3583
|
});
|
|
@@ -198,6 +198,7 @@ const FocusMixinImplementation = superclass =>
|
|
|
198
198
|
* @private
|
|
199
199
|
*/
|
|
200
200
|
__teardownEventsForFocusMixin() {
|
|
201
|
+
if (!this._focusableNode) return;
|
|
201
202
|
this._focusableNode?.removeEventListener(
|
|
202
203
|
'focus',
|
|
203
204
|
/** @type {EventListenerOrEventListenerObject} */ (this.__redispatchFocus),
|
|
@@ -52,7 +52,7 @@ const FormControlMixinImplementation = superclass =>
|
|
|
52
52
|
* @type {string}
|
|
53
53
|
*/
|
|
54
54
|
get label() {
|
|
55
|
-
return this.__label
|
|
55
|
+
return this.__label ?? (this._labelNode?.textContent || '');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -71,7 +71,7 @@ const FormControlMixinImplementation = superclass =>
|
|
|
71
71
|
* @type {string}
|
|
72
72
|
*/
|
|
73
73
|
get helpText() {
|
|
74
|
-
return this.__helpText
|
|
74
|
+
return this.__helpText ?? (this._helpTextNode?.textContent || '');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
@@ -158,6 +158,21 @@ const FormControlMixinImplementation = superclass =>
|
|
|
158
158
|
*/
|
|
159
159
|
static enabledWarnings = super.enabledWarnings?.filter(w => w !== 'change-in-update') || [];
|
|
160
160
|
|
|
161
|
+
// N.B. add these label/helpText props for types + ce manifest output
|
|
162
|
+
// (explicity setting them to undefined does not have desired result)
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The label text for the input node.
|
|
166
|
+
* When no value is defined, textContent of [slot=label] will be used
|
|
167
|
+
* @property {string} label
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The helpt text for the input node.
|
|
172
|
+
* When no value is defined, textContent of [slot=help-text] will be used
|
|
173
|
+
* @property {string} helpText
|
|
174
|
+
*/
|
|
175
|
+
|
|
161
176
|
constructor() {
|
|
162
177
|
super();
|
|
163
178
|
|
|
@@ -171,26 +186,12 @@ const FormControlMixinImplementation = superclass =>
|
|
|
171
186
|
*/
|
|
172
187
|
this.readOnly = false;
|
|
173
188
|
|
|
174
|
-
/**
|
|
175
|
-
* The label text for the input node.
|
|
176
|
-
* When no value is defined, textContent of [slot=label] will be used
|
|
177
|
-
* @type {string}
|
|
178
|
-
*/
|
|
179
|
-
this.label = '';
|
|
180
|
-
|
|
181
189
|
/**
|
|
182
190
|
* The label will only be visible for srceen readers when true
|
|
183
191
|
* @type {boolean}
|
|
184
192
|
*/
|
|
185
193
|
this.labelSrOnly = false;
|
|
186
194
|
|
|
187
|
-
/**
|
|
188
|
-
* The helpt text for the input node.
|
|
189
|
-
* When no value is defined, textContent of [slot=help-text] will be used
|
|
190
|
-
* @type {string}
|
|
191
|
-
*/
|
|
192
|
-
this.helpText = '';
|
|
193
|
-
|
|
194
195
|
/**
|
|
195
196
|
* The model value is the result of the parser function(when available).
|
|
196
197
|
* It should be considered as the internal value used for validation and reasoning/logic.
|
|
@@ -293,11 +294,15 @@ const FormControlMixinImplementation = superclass =>
|
|
|
293
294
|
);
|
|
294
295
|
}
|
|
295
296
|
|
|
296
|
-
if (changedProperties.has('label') && this.__label && this._labelNode) {
|
|
297
|
+
if (changedProperties.has('label') && this.__label !== undefined && this._labelNode) {
|
|
297
298
|
this._labelNode.textContent = this.label;
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
if (
|
|
301
|
+
if (
|
|
302
|
+
changedProperties.has('helpText') &&
|
|
303
|
+
this.__helpText !== undefined &&
|
|
304
|
+
this._helpTextNode
|
|
305
|
+
) {
|
|
301
306
|
this._helpTextNode.textContent = this.helpText;
|
|
302
307
|
}
|
|
303
308
|
|
|
@@ -420,6 +420,22 @@ export const ValidateMixinImplementation = superclass =>
|
|
|
420
420
|
await this.__executeValidators();
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
/**
|
|
424
|
+
* @param {Validator} validator
|
|
425
|
+
* @returns {boolean}
|
|
426
|
+
*/
|
|
427
|
+
#isInstanceOfRequiredValidator(validator) {
|
|
428
|
+
// walk prototype and check if validatorName is 'Required'
|
|
429
|
+
let validatorLvlInPrototype = validator;
|
|
430
|
+
while (validatorLvlInPrototype) {
|
|
431
|
+
const ctor = /** @type {typeof Validator} */ (validatorLvlInPrototype.constructor);
|
|
432
|
+
if (ctor.validatorName === 'Required') return true;
|
|
433
|
+
|
|
434
|
+
validatorLvlInPrototype = Object.getPrototypeOf(validatorLvlInPrototype);
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
423
439
|
/**
|
|
424
440
|
* @desc step a1-3 + b (as explained in `validate()`)
|
|
425
441
|
*/
|
|
@@ -482,7 +498,7 @@ export const ValidateMixinImplementation = superclass =>
|
|
|
482
498
|
for (const v of this._allValidators) {
|
|
483
499
|
if (/** @type {MetaValidator} */ (v)?.executeOnResults) {
|
|
484
500
|
metaValidators.push(/** @type {MetaValidator} */ (v));
|
|
485
|
-
} else if (
|
|
501
|
+
} else if (this.#isInstanceOfRequiredValidator(v)) {
|
|
486
502
|
// Required validator was already handled
|
|
487
503
|
} else if (/** @type {typeof Validator} */ (v.constructor).async) {
|
|
488
504
|
asyncValidators.push(v);
|
|
@@ -86,6 +86,40 @@ describe('FormControlMixin', () => {
|
|
|
86
86
|
expect(el.label).to.equal('');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
it('supports empty string as label', async () => {
|
|
90
|
+
const el = /** @type {FormControlMixinClass} */ (
|
|
91
|
+
await fixture(html`
|
|
92
|
+
<${tag} label="With Value">${inputSlot}</${tag}>
|
|
93
|
+
`)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(el.label).to.equal('With Value');
|
|
97
|
+
// @ts-expect-error
|
|
98
|
+
expect(el._labelNode.innerHTML).to.equal('With Value');
|
|
99
|
+
|
|
100
|
+
el.label = '';
|
|
101
|
+
await el.updateComplete;
|
|
102
|
+
|
|
103
|
+
expect(el.label).to.equal('');
|
|
104
|
+
// @ts-expect-error
|
|
105
|
+
expect(el._labelNode.innerHTML).to.equal('');
|
|
106
|
+
|
|
107
|
+
// Reset
|
|
108
|
+
el.label = 'With Value';
|
|
109
|
+
await el.updateComplete;
|
|
110
|
+
|
|
111
|
+
expect(el.label).to.equal('With Value');
|
|
112
|
+
// @ts-expect-error
|
|
113
|
+
expect(el._labelNode.innerHTML).to.equal('With Value');
|
|
114
|
+
|
|
115
|
+
el.setAttribute('label', '');
|
|
116
|
+
await el.updateComplete;
|
|
117
|
+
|
|
118
|
+
expect(el.label).to.equal('');
|
|
119
|
+
// @ts-expect-error
|
|
120
|
+
expect(el._labelNode.innerHTML).to.equal('');
|
|
121
|
+
});
|
|
122
|
+
|
|
89
123
|
/**
|
|
90
124
|
* N.B. For platform controls, the same would be achieved with <input aria-label="My label">
|
|
91
125
|
* However, since FormControl is usually not the activeElement (_inputNode is), this
|
|
@@ -176,6 +210,40 @@ describe('FormControlMixin', () => {
|
|
|
176
210
|
);
|
|
177
211
|
expect(el.helpText).to.equal('');
|
|
178
212
|
});
|
|
213
|
+
|
|
214
|
+
it('supports empty string as help-text', async () => {
|
|
215
|
+
const el = /** @type {FormControlMixinClass} */ (
|
|
216
|
+
await fixture(html`
|
|
217
|
+
<${tag} help-text="With Value">${inputSlot}</${tag}>
|
|
218
|
+
`)
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(el.helpText).to.equal('With Value');
|
|
222
|
+
// @ts-expect-error
|
|
223
|
+
expect(el._helpTextNode.innerHTML).to.equal('With Value');
|
|
224
|
+
|
|
225
|
+
el.helpText = '';
|
|
226
|
+
await el.updateComplete;
|
|
227
|
+
|
|
228
|
+
expect(el.helpText).to.equal('');
|
|
229
|
+
// @ts-expect-error
|
|
230
|
+
expect(el._helpTextNode.innerHTML).to.equal('');
|
|
231
|
+
|
|
232
|
+
// Reset
|
|
233
|
+
el.helpText = 'With Value';
|
|
234
|
+
await el.updateComplete;
|
|
235
|
+
|
|
236
|
+
expect(el.helpText).to.equal('With Value');
|
|
237
|
+
// @ts-expect-error
|
|
238
|
+
expect(el._helpTextNode.innerHTML).to.equal('With Value');
|
|
239
|
+
|
|
240
|
+
el.setAttribute('help-text', '');
|
|
241
|
+
await el.updateComplete;
|
|
242
|
+
|
|
243
|
+
expect(el.helpText).to.equal('');
|
|
244
|
+
// @ts-expect-error
|
|
245
|
+
expect(el._helpTextNode.innerHTML).to.equal('');
|
|
246
|
+
});
|
|
179
247
|
});
|
|
180
248
|
|
|
181
249
|
describe('Accessibility', () => {
|
|
@@ -130,6 +130,7 @@ export function runValidateMixinSuite(customConfig) {
|
|
|
130
130
|
expect(el.hasFeedbackFor).to.deep.equal(['error']);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
// TODO: keep this use case for backwards compatibility or just required extending Required?
|
|
133
134
|
it('determines whether the "Required" validator was already handled by judging the validatorName', async () => {
|
|
134
135
|
class BundledValidator extends EventTarget {
|
|
135
136
|
static ['_$isValidator$'] = true;
|
|
@@ -163,9 +164,7 @@ export function runValidateMixinSuite(customConfig) {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
class BundledRequired extends BundledValidator {
|
|
166
|
-
static
|
|
167
|
-
return 'Required';
|
|
168
|
-
}
|
|
167
|
+
static validatorName = 'Required';
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
const el = /** @type {ValidateElement} */ (
|
|
@@ -180,6 +179,23 @@ export function runValidateMixinSuite(customConfig) {
|
|
|
180
179
|
expect(el.hasFeedbackFor).to.deep.equal([]);
|
|
181
180
|
});
|
|
182
181
|
|
|
182
|
+
it('determines whether the "Required" validator was already handled by judging the validatorName', async () => {
|
|
183
|
+
class MyRequired extends Required {
|
|
184
|
+
static validatorName = 'SomethingOtherThanRequired';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const el = /** @type {ValidateElement} */ (
|
|
188
|
+
await fixture(html`
|
|
189
|
+
<${tag}
|
|
190
|
+
.validators=${[new MyRequired()]}
|
|
191
|
+
.modelValue=${'myValue'}
|
|
192
|
+
>${lightDom}</${tag}>
|
|
193
|
+
`)
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(el.hasFeedbackFor).to.deep.equal([]);
|
|
197
|
+
});
|
|
198
|
+
|
|
183
199
|
it('determines whether the passed Validators are ResultValidators judging by the presence of "executeOnResults"', async () => {
|
|
184
200
|
class ValidateElementWithSuccessType extends ValidateElement {
|
|
185
201
|
static get validationTypes() {
|