@lion/ui 0.7.6 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +1 -2
  2. package/components/button/test-suites/LionButton.suite.js +3 -1
  3. package/components/combobox/src/LionCombobox.js +33 -9
  4. package/components/combobox/test/lion-combobox.test.js +134 -7
  5. package/components/dialog/test/lion-dialog.test.js +59 -1
  6. package/components/input-datepicker/test/lion-input-datepicker.test.js +3 -3
  7. package/components/input-file/src/FileHandle.js +1 -1
  8. package/components/input-file/src/LionInputFile.js +1 -3
  9. package/components/input-file/test/lion-input-file.test.js +11 -14
  10. package/components/overlays/src/OverlayController.js +13 -7
  11. package/components/overlays/src/OverlayMixin.js +5 -1
  12. package/components/overlays/test/OverlayController.test.js +46 -0
  13. package/components/select-rich/src/LionSelectRich.js +12 -0
  14. package/components/select-rich/test/lion-select-rich.test.js +26 -0
  15. package/custom-elements.json +60 -55
  16. package/dist-types/components/button/test-suites/LionButton.suite.d.ts.map +1 -1
  17. package/dist-types/components/combobox/src/LionCombobox.d.ts +9 -0
  18. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  19. package/dist-types/components/input-file/src/LionInputFile.d.ts.map +1 -1
  20. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  21. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  22. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  23. package/dist-types/tsconfig.tsbuildinfo +1 -1
  24. package/docs/components/button/use-cases.md +2 -2
  25. package/docs/components/calendar/use-cases.md +4 -4
  26. package/docs/components/checkbox-group/api-table.md +49 -55
  27. package/docs/components/checkbox-group/overview.md +5 -5
  28. package/docs/components/checkbox-group/use-cases.md +33 -33
  29. package/docs/components/collapsible/overview.md +1 -1
  30. package/docs/components/collapsible/use-cases.md +3 -3
  31. package/docs/components/combobox/api-table.md +84 -89
  32. package/docs/components/combobox/extensions.md +20 -20
  33. package/docs/components/combobox/overview.md +1 -1
  34. package/docs/components/combobox/use-cases.md +6 -6
  35. package/docs/components/dialog/api-table.md +3 -3
  36. package/docs/components/dialog/overview.md +2 -2
  37. package/docs/components/dialog/src/external-dialog.js +32 -0
  38. package/docs/components/dialog/src/slots-dialog-content.js +1 -1
  39. package/docs/components/dialog/src/styled-dialog-content.js +1 -1
  40. package/docs/components/dialog/use-cases.md +49 -3
  41. package/docs/components/drawer/use-cases.md +3 -3
  42. package/docs/components/fieldset/api-table.md +45 -51
  43. package/docs/components/fieldset/overview.md +7 -7
  44. package/docs/components/fieldset/use-cases.md +15 -15
  45. package/docs/components/form/overview.md +7 -7
  46. package/docs/components/form/use-cases.md +4 -4
  47. package/docs/components/icon/use-cases.md +1 -1
  48. package/docs/components/input/overview.md +1 -1
  49. package/docs/components/input/use-cases.md +3 -3
  50. package/docs/components/input-amount/overview.md +3 -3
  51. package/docs/components/input-amount/use-cases.md +8 -8
  52. package/docs/components/input-date/overview.md +3 -3
  53. package/docs/components/input-date/use-cases.md +8 -8
  54. package/docs/components/input-datepicker/api-table.md +1 -1
  55. package/docs/components/input-datepicker/overview.md +5 -5
  56. package/docs/components/input-datepicker/use-cases.md +5 -5
  57. package/docs/components/input-email/overview.md +2 -2
  58. package/docs/components/input-email/use-cases.md +3 -3
  59. package/docs/components/input-file/overview.md +1 -1
  60. package/docs/components/input-iban/overview.md +2 -2
  61. package/docs/components/input-iban/use-cases.md +8 -8
  62. package/docs/components/input-range/overview.md +2 -2
  63. package/docs/components/input-stepper/api-table.md +65 -69
  64. package/docs/components/input-stepper/overview.md +1 -1
  65. package/docs/components/input-tel/overview.md +1 -1
  66. package/docs/components/input-tel/use-cases.md +9 -9
  67. package/docs/components/input-tel-dropdown/api-table.md +70 -73
  68. package/docs/components/input-tel-dropdown/extensions.md +1 -1
  69. package/docs/components/input-tel-dropdown/overview.md +1 -1
  70. package/docs/components/input-tel-dropdown/use-cases.md +5 -5
  71. package/docs/components/listbox/api-table.md +64 -70
  72. package/docs/components/listbox/overview.md +5 -5
  73. package/docs/components/listbox/use-cases.md +61 -61
  74. package/docs/components/pagination/use-cases.md +5 -5
  75. package/docs/components/radio-group/api-table.md +49 -55
  76. package/docs/components/radio-group/overview.md +6 -6
  77. package/docs/components/radio-group/use-cases.md +18 -18
  78. package/docs/components/select/use-cases.md +2 -2
  79. package/docs/components/select-rich/api-table.md +79 -85
  80. package/docs/components/select-rich/overview.md +3 -3
  81. package/docs/components/select-rich/use-cases.md +33 -33
  82. package/docs/components/steps/overview.md +4 -4
  83. package/docs/components/steps/use-cases.md +7 -7
  84. package/docs/components/tabs/examples.md +1 -1
  85. package/docs/components/tabs/use-cases.md +1 -1
  86. package/docs/components/textarea/use-cases.md +3 -3
  87. package/docs/components/tooltip/api-table.md +3 -3
  88. package/docs/components/tooltip/use-cases.md +6 -6
  89. package/docs/fundamentals/index.md +4 -4
  90. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  91. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  92. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  93. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  94. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  95. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  96. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  97. package/docs/fundamentals/rationales/accessibility.md +1 -1
  98. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  99. package/docs/fundamentals/systems/core/overview.md +1 -1
  100. package/docs/fundamentals/systems/form/formatting-and-parsing.md +8 -8
  101. package/docs/fundamentals/systems/form/model-value.md +1 -1
  102. package/docs/fundamentals/systems/form/overview.md +28 -28
  103. package/docs/fundamentals/systems/form/use-cases.md +19 -19
  104. package/docs/fundamentals/systems/form/validate.md +50 -50
  105. package/docs/fundamentals/systems/localize/api-table.md +10 -17
  106. package/docs/fundamentals/systems/localize/overview.md +4 -4
  107. package/docs/fundamentals/systems/localize/use-cases.md +4 -4
  108. package/docs/fundamentals/systems/overlays/_configuration-positioning.md +4 -4
  109. package/docs/fundamentals/systems/overlays/api-table.md +34 -90
  110. package/docs/fundamentals/systems/overlays/assets/umbrella-form.js +9 -9
  111. package/docs/fundamentals/systems/overlays/configuration.md +30 -30
  112. package/docs/fundamentals/systems/overlays/overview.md +6 -6
  113. package/docs/fundamentals/systems/overlays/use-cases.md +30 -30
  114. package/docs/fundamentals/tools/ajax/use-cases.md +20 -20
  115. package/docs/fundamentals/tools/helpers/action-logger.md +3 -3
  116. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  117. package/docs/fundamentals/tools/singleton-manager/example-complex/demo-app.js +2 -2
  118. package/docs/fundamentals/tools/singleton-manager/example-fail/demo-app.js +2 -2
  119. package/docs/fundamentals/tools/singleton-manager/example-success/demo-app.js +2 -2
  120. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  121. package/docs/guides/how-to/create-a-custom-field.md +7 -7
  122. package/docs/guides/how-to/create-a-form.md +4 -4
  123. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  124. package/docs/guides/index.md +8 -8
  125. package/docs/guides/principles/index.md +5 -5
  126. package/docs/guides/principles/scoped-elements.md +1 -1
  127. package/docs/guides/principles/styling.md +1 -1
  128. package/package.json +1 -1
package/README.md CHANGED
@@ -47,8 +47,7 @@ They provide an unopinionated, white-label layer that can be extended to your ow
47
47
  - **Modern Code:** Lion is distributes as pure es modules.
48
48
  - **Exposes functions/classes and Web Components:** Ships a functionality in it's most appropriate form.
49
49
 
50
- > Note: Our demos may look a little bland but that is on purpose. They only come with functional stylings.
51
- > This makes sense as the main use case is to extend those components and if you do you do not want to override existing stylings.
50
+ > Note: Our demo examples might look simple and not very stylish. This is on purpose. They are designed to be basic so you can easily add your own styles to them to match your intended design, without dealing with styles that are already there.
52
51
 
53
52
  <p align="center">
54
53
  <a href="https://lion-web.netlify.app/guides/"><strong>Explore the Lion Guides&nbsp;&nbsp;▶</strong></a>
@@ -52,7 +52,9 @@ export function LionButtonSuite({ klass = LionButton } = {}) {
52
52
 
53
53
  describe('Active', () => {
54
54
  it('updates "active" attribute on host when mousedown/mouseup on button', async () => {
55
- const el = /** @type {LionButton} */ (await fixture(html`<${tagButton}>foo</${tagButton}`));
55
+ const el = /** @type {LionButton} */ (
56
+ await fixture(html`<${tagButton}>foo</${tagButton}>`)
57
+ );
56
58
  el.dispatchEvent(new Event('mousedown'));
57
59
 
58
60
  expect(el.active).to.be.true;
@@ -22,6 +22,7 @@ const matchA11ySpanReverseFns = new WeakMap();
22
22
  * @typedef {import('@lion/ui/types/form-core.js').ChoiceInputHost} ChoiceInputHost
23
23
  * @typedef {import('@lion/ui/types/form-core.js').FormControlHost} FormControlHost
24
24
  * @typedef {import('../types/SelectionDisplay.js').SelectionDisplay} SelectionDisplay
25
+ * @typedef {LionOption & { onFilterUnmatch?:function; onFilterMatch?:function }} OptionWithFilterFn
25
26
  */
26
27
 
27
28
  /**
@@ -201,8 +202,30 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
201
202
 
202
203
  reset() {
203
204
  super.reset();
204
- // @ts-ignore _initialModelValue comes from ListboxMixin
205
- this.value = this._initialModelValue;
205
+ if (!this.multipleChoice) {
206
+ // @ts-ignore _initialModelValue comes from ListboxMixin
207
+ this.value = this._initialModelValue;
208
+ }
209
+ this._resetListboxOptions();
210
+ }
211
+
212
+ /**
213
+ * @protected
214
+ */
215
+ _resetListboxOptions() {
216
+ this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, idx) => {
217
+ this._unhighlightMatchedOption(option);
218
+ if (!this.showAllOnEmpty || !this.opened) {
219
+ // eslint-disable-next-line no-param-reassign
220
+ option.style.display = 'none';
221
+ } else {
222
+ // eslint-disable-next-line no-param-reassign
223
+ option.style.display = '';
224
+ option.setAttribute('aria-posinset', `${idx + 1}`);
225
+ option.setAttribute('aria-setsize', `${this.formElements.length}`);
226
+ option.removeAttribute('aria-hidden');
227
+ }
228
+ });
206
229
  }
207
230
 
208
231
  /**
@@ -704,11 +727,13 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
704
727
  */
705
728
  _listboxOnClick(ev) {
706
729
  super._listboxOnClick(ev);
707
-
708
730
  this._inputNode.focus();
709
731
  if (!this.multipleChoice) {
710
732
  this.activeIndex = -1;
711
733
  this.opened = false;
734
+ } else {
735
+ this._inputNode.value = '';
736
+ this._resetListboxOptions();
712
737
  }
713
738
  }
714
739
 
@@ -897,7 +922,6 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
897
922
  const autoselect = this._autoSelectCondition();
898
923
  const noFilter = this.autocomplete === 'inline' || this.autocomplete === 'none';
899
924
 
900
- /** @typedef {LionOption & { onFilterUnmatch?:function, onFilterMatch?:function }} OptionWithFilterFn */
901
925
  this.formElements.forEach((/** @type {OptionWithFilterFn} */ option, i) => {
902
926
  // [1]. Decide whether option should be shown
903
927
  const matches = this.matchCondition(option, curValue);
@@ -983,7 +1007,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
983
1007
 
984
1008
  // [8]. These values will help computing autofill intentions next autocomplete cycle
985
1009
  this.__prevCboxValueNonSelected = curValue;
986
- // See test 'computation of "user intends autofill" works correctly afer autofill'
1010
+ // See test 'computation of "user intends autofill" works correctly after autofill'
987
1011
  this.__prevCboxValue = this._inputNode.value;
988
1012
  this.__hadSelectionLastAutofill =
989
1013
  this._inputNode.value.length !== this._inputNode.selectionStart;
@@ -1120,16 +1144,16 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
1120
1144
  ev.preventDefault();
1121
1145
 
1122
1146
  this.modelValue = this.parser([...this.modelValue, this._inputNode.value]);
1123
-
1124
1147
  this._inputNode.value = '';
1125
1148
  this.opened = false;
1126
1149
  } else {
1127
1150
  super._listboxOnKeyDown(ev);
1128
- // TODO: should we clear the input value here when allowCustomChoice is false?
1129
- // For now, we don't...
1151
+ this._resetListboxOptions();
1130
1152
  }
1131
1153
  if (!this.multipleChoice) {
1132
1154
  this.opened = false;
1155
+ } else {
1156
+ this._inputNode.value = '';
1133
1157
  }
1134
1158
  break;
1135
1159
  default: {
@@ -1168,7 +1192,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
1168
1192
  .filter(option => diff.includes(option.choiceValue))
1169
1193
  .map(option => this._getTextboxValueFromOption(option))
1170
1194
  .join(' ');
1171
- this._setTextboxValue(newValue); // or last selected value?
1195
+ this._setTextboxValue(newValue);
1172
1196
  }
1173
1197
  }
1174
1198
 
@@ -81,6 +81,54 @@ describe('lion-combobox', () => {
81
81
  await performChecks();
82
82
  });
83
83
 
84
+ it('hides all options on reset()', async () => {
85
+ const el = /** @type {LionCombobox} */ (
86
+ await fixture(html`
87
+ <lion-combobox name="foo">
88
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
89
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
90
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
91
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
92
+ </lion-combobox>
93
+ `)
94
+ );
95
+
96
+ const options = el.formElements;
97
+ const visibleOptions = () => options.filter(o => o.style.display !== 'none');
98
+
99
+ mimicUserTyping(el, 'cha');
100
+ await el.updateComplete;
101
+ expect(visibleOptions().length).to.equal(1);
102
+ el.reset();
103
+ await el.updateComplete;
104
+ expect(visibleOptions().length).to.equal(0);
105
+ });
106
+
107
+ it('shows all options on reset() when showAllOnEmpty is set to true and overlay was open', async () => {
108
+ const el = /** @type {LionCombobox} */ (
109
+ await fixture(html`
110
+ <lion-combobox name="foo" show-all-on-empty>
111
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
112
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
113
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
114
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
115
+ </lion-combobox>
116
+ `)
117
+ );
118
+
119
+ const options = el.formElements;
120
+ const visibleOptions = () => options.filter(o => o.getAttribute('aria-hidden') !== 'true');
121
+
122
+ mimicUserTyping(el, 'cha');
123
+ await el.updateComplete;
124
+ expect(visibleOptions().length).to.equal(1);
125
+ expect(el.opened).to.be.true;
126
+
127
+ el.reset();
128
+ await el.updateComplete;
129
+ expect(visibleOptions().length).to.equal(4);
130
+ });
131
+
84
132
  it('hides listbox on click/enter (when multiple-choice is false)', async () => {
85
133
  const el = /** @type {LionCombobox} */ (
86
134
  await fixture(html`
@@ -467,13 +515,12 @@ describe('lion-combobox', () => {
467
515
  mimicUserTyping(el, 'a');
468
516
  await el.updateComplete;
469
517
 
470
- const visibleOptions = options.filter(o => o.style.display !== 'none');
471
- expect(visibleOptions.length).to.equal(3, 'after input');
518
+ const visibleOptions = () => options.filter(o => o.style.display !== 'none');
519
+ expect(visibleOptions().length).to.equal(3, 'after input');
472
520
 
473
521
  el.clear();
474
522
  await el.updateComplete;
475
- const visibleOptions2 = options.filter(o => o.style.display !== 'none');
476
- expect(visibleOptions2.length).to.equal(0, 'after clear');
523
+ expect(visibleOptions().length).to.equal(0, 'after clear');
477
524
  });
478
525
 
479
526
  it('resets modelValue and textbox value on reset()', async () => {
@@ -508,7 +555,7 @@ describe('lion-combobox', () => {
508
555
  el2.reset();
509
556
  expect(el2.modelValue).to.deep.equal(['Artichoke']);
510
557
  // @ts-ignore [allow-protected] in test
511
- expect(el2._inputNode.value).to.equal('Artichoke');
558
+ expect(el2._inputNode.value).to.equal('');
512
559
  });
513
560
 
514
561
  it('syncs textbox to modelValue', async () => {
@@ -735,7 +782,7 @@ describe('lion-combobox', () => {
735
782
  await el.updateComplete;
736
783
  });
737
784
 
738
- it('allows manyu custom selections when multi-choice when requireOptionMatch is false', async () => {
785
+ it('allows many custom selections when multi-choice when requireOptionMatch is false', async () => {
739
786
  const el = /** @type {LionCombobox} */ (
740
787
  await fixture(html`
741
788
  <lion-combobox
@@ -2088,7 +2135,7 @@ describe('lion-combobox', () => {
2088
2135
  });
2089
2136
  });
2090
2137
 
2091
- it('highlights first occcurence of matching option', async () => {
2138
+ it('highlights first occurrence of matching option', async () => {
2092
2139
  const el = /** @type {LionCombobox} */ (
2093
2140
  await fixture(html`
2094
2141
  <lion-combobox name="foo" match-mode="all">
@@ -2170,6 +2217,28 @@ describe('lion-combobox', () => {
2170
2217
  );
2171
2218
  });
2172
2219
 
2220
+ it('resets removes highlights on reset()', async () => {
2221
+ const el = /** @type {LionCombobox} */ (
2222
+ await fixture(html`
2223
+ <lion-combobox name="foo" match-mode="all">
2224
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
2225
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
2226
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
2227
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
2228
+ </lion-combobox>
2229
+ `)
2230
+ );
2231
+ const options = el.formElements;
2232
+
2233
+ mimicUserTyping(/** @type {LionCombobox} */ (el), 'c');
2234
+
2235
+ await el.updateComplete;
2236
+ expect(options[0]).lightDom.to.equal(`<span aria-label="Artichoke">Arti<b>c</b>hoke</span>`);
2237
+ el.reset();
2238
+ await el.updateComplete;
2239
+ expect(options[0]).lightDom.to.equal(`Artichoke`);
2240
+ });
2241
+
2173
2242
  it('synchronizes textbox when autocomplete is "inline" or "both"', async () => {
2174
2243
  const el = /** @type {LionCombobox} */ (
2175
2244
  await fixture(html`
@@ -3018,6 +3087,64 @@ describe('lion-combobox', () => {
3018
3087
  expect(el.opened).to.equal(true);
3019
3088
  });
3020
3089
 
3090
+ it('clears textbox after selection of a new item on [enter]', async () => {
3091
+ const el = /** @type {LionCombobox} */ (
3092
+ await fixture(html`
3093
+ <lion-combobox name="foo" multiple-choice>
3094
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
3095
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
3096
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
3097
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
3098
+ </lion-combobox>
3099
+ `)
3100
+ );
3101
+
3102
+ const { _inputNode } = getComboboxMembers(el);
3103
+ const options = el.formElements;
3104
+
3105
+ mimicUserTyping(el, 'art');
3106
+ await el.updateComplete;
3107
+
3108
+ expect(el.opened).to.equal(true);
3109
+ const visibleOptions = () => options.filter(o => o.style.display !== 'none');
3110
+ expect(visibleOptions().length).to.equal(1);
3111
+
3112
+ // N.B. we do only trigger keydown here (and not mimicKeypress (both keyup and down)),
3113
+ // because this closely mimics what happens in the browser
3114
+ _inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
3115
+ expect(el.opened).to.equal(true);
3116
+ expect(visibleOptions().length).to.equal(0);
3117
+ expect(_inputNode.value).to.equal('');
3118
+ });
3119
+
3120
+ it('clears textbox after selection of a new item on click', async () => {
3121
+ const el = /** @type {LionCombobox} */ (
3122
+ await fixture(html`
3123
+ <lion-combobox name="foo" multiple-choice>
3124
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
3125
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
3126
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
3127
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
3128
+ </lion-combobox>
3129
+ `)
3130
+ );
3131
+
3132
+ const { _inputNode } = getComboboxMembers(el);
3133
+ const options = el.formElements;
3134
+
3135
+ mimicUserTyping(el, 'art');
3136
+ await el.updateComplete;
3137
+
3138
+ expect(el.opened).to.equal(true);
3139
+ const visibleOptions = () => options.filter(o => o.style.display !== 'none');
3140
+ expect(visibleOptions().length).to.equal(1);
3141
+
3142
+ visibleOptions()[0].click();
3143
+ expect(el.opened).to.equal(true);
3144
+ expect(visibleOptions().length).to.equal(0);
3145
+ expect(_inputNode.value).to.equal('');
3146
+ });
3147
+
3021
3148
  it('submits form on [Enter] when listbox is closed', async () => {
3022
3149
  const submitSpy = sinon.spy(e => e.preventDefault());
3023
3150
  const el = /** @type {HTMLFormElement} */ (
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable lit-a11y/no-autofocus */
2
- import { expect, fixture as _fixture, html, unsafeStatic } from '@open-wc/testing';
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 '@lion/ui/define/lion-dialog.js';
5
5
 
@@ -127,5 +127,63 @@ describe('lion-dialog', () => {
127
127
  invokerNode.click();
128
128
  expect(document.activeElement).to.equal(invokerNode);
129
129
  });
130
+
131
+ it('opened-changed event should send detail object with opened state', async () => {
132
+ const el = /** @type {LionDialog} */ await fixture(html`
133
+ <lion-dialog .config=${{ trapsKeyboardFocus: false }}>
134
+ <button slot="invoker">invoker button</button>
135
+ <div slot="content">
136
+ <label for="myInput">Label</label>
137
+ <input id="myInput" autofocus />
138
+ </div>
139
+ </lion-dialog>
140
+ `);
141
+
142
+ el.setAttribute('opened', '');
143
+ expect(el.opened).to.be.true;
144
+
145
+ el.addEventListener('opened-changed', e => {
146
+ // @ts-expect-error [allow-detail-since-custom-event]
147
+ expect(e.detail.opened).to.be.false;
148
+ });
149
+ el.removeAttribute('opened');
150
+ });
151
+
152
+ it("opened-changed event's target should point to lion-dialog", async () => {
153
+ const el = /** @type {LionDialog} */ await fixture(html`
154
+ <lion-dialog .config=${{ trapsKeyboardFocus: false }}>
155
+ <button slot="invoker">invoker button</button>
156
+ <div slot="content">
157
+ <label for="myInput">Label</label>
158
+ <input id="myInput" autofocus />
159
+ </div>
160
+ </lion-dialog>
161
+ `);
162
+
163
+ el.addEventListener('opened-changed', e => {
164
+ expect(e.target).to.equal(el);
165
+ });
166
+ el.setAttribute('opened', '');
167
+ });
168
+ });
169
+
170
+ describe('Accessibility', () => {
171
+ it('does not add [aria-expanded] to invoker button', async () => {
172
+ const el = await fixture(
173
+ html` <lion-dialog>
174
+ <div slot="content" class="dialog">Hey there</div>
175
+ <button slot="invoker">Popup button</button>
176
+ </lion-dialog>`,
177
+ );
178
+ const invokerButton = /** @type {HTMLElement} */ (el.querySelector('[slot="invoker"]'));
179
+
180
+ expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
181
+ await invokerButton.click();
182
+ await aTimeout(0);
183
+ expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
184
+ await invokerButton.click();
185
+ await aTimeout(0);
186
+ expect(invokerButton.getAttribute('aria-expanded')).to.equal(null);
187
+ });
130
188
  });
131
189
  });
@@ -495,11 +495,11 @@ describe('<lion-input-datepicker>', () => {
495
495
  const el = await fixture(html`<lion-input-datepicker></lion-input-datepicker>`);
496
496
  const elObj = new DatepickerInputObject(el);
497
497
 
498
- expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
498
+ expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal(null);
499
499
  await elObj.openCalendar();
500
- expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('true');
500
+ expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal(null);
501
501
  await elObj.closeCalendar();
502
- expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
502
+ expect(elObj.invokerEl.getAttribute('aria-expanded')).to.equal(null);
503
503
  });
504
504
 
505
505
  it('is accessible when closed', async () => {
@@ -53,7 +53,7 @@ export class FileHandle {
53
53
  */
54
54
  // eslint-disable-next-line class-methods-use-this
55
55
  _getFileNameExtension(fileName) {
56
- return fileName.slice(fileName.lastIndexOf('.') + 1);
56
+ return fileName.slice(fileName.lastIndexOf('.'));
57
57
  }
58
58
 
59
59
  // TODO: seems to suggest upload is going on...
@@ -232,7 +232,7 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
232
232
  /** @type {string[]} */
233
233
  let allowedFileExtensions = [];
234
234
  if (this.accept) {
235
- const acceptedFiles = this.accept.replace(/\s+/g, '').replace(/\.+/g, '').split(',');
235
+ const acceptedFiles = this.accept.replace(/\s+/g, '').split(',');
236
236
  allowedFileTypes = acceptedFiles.filter(acceptedFile => acceptedFile.includes('/'));
237
237
  allowedFileExtensions = acceptedFiles.filter(acceptedFile => !acceptedFile.includes('/'));
238
238
  }
@@ -692,8 +692,6 @@ export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField))
692
692
 
693
693
  if (allowedFileExtensions.length) {
694
694
  array = allowedFileExtensions;
695
- // eslint-disable-next-line no-return-assign
696
- array = array.map(item => (item = `.${item}`));
697
695
  lastItem = array.pop();
698
696
  arrayLength = array.length;
699
697
  } else if (allowedFileTypes.length) {
@@ -158,6 +158,17 @@ describe('lion-input-file', () => {
158
158
  })
159
159
  );
160
160
 
161
+ it('error should not be there when the file extensions are accepted', async () => {
162
+ const el = await fixture(html`
163
+ <lion-input-file label="Select" accept=".txt"></lion-input-file>
164
+ `);
165
+
166
+ mimicSelectFile(el, [fileWrongType]);
167
+ await el.updateComplete;
168
+
169
+ expect(el.hasFeedbackFor.length).to.equal(0);
170
+ });
171
+
161
172
  it('should not be added to the selected list', async () => {
162
173
  const el = await fixture(html`
163
174
  <lion-input-file label="Select" accept="text/plain"></lion-input-file>
@@ -343,20 +354,6 @@ describe('lion-input-file', () => {
343
354
  expect(error.message).to.equal('Please select a .jpg, .png or .pdf file with max 500MB.');
344
355
  });
345
356
  });
346
-
347
- it('error message should add all file extensions to the validator message also works without dots "."', async () => {
348
- const el = await fixture(html`
349
- <lion-input-file label="Select" accept="jpg, png, pdf"></lion-input-file>
350
- `);
351
-
352
- mimicSelectFile(el, [fileWrongType]);
353
- await el.updateComplete;
354
-
355
- // @ts-expect-error [allow-protected-in-test]
356
- el._selectedFilesMetaData[0].validationFeedback?.forEach(error => {
357
- expect(error.message).to.equal('Please select a .jpg, .png or .pdf file with max 500MB.');
358
- });
359
- });
360
357
  });
361
358
 
362
359
  describe('invalid file sizes', async () => {
@@ -640,13 +640,14 @@ export class OverlayController extends EventTarget {
640
640
  __setupTeardownAccessibility({ phase }) {
641
641
  if (phase === 'init') {
642
642
  this.__storeOriginalAttrs(this.contentNode, ['role', 'id']);
643
+ const isModal = this.trapsKeyboardFocus;
643
644
 
644
645
  if (this.invokerNode) {
645
- this.__storeOriginalAttrs(this.invokerNode, [
646
- 'aria-expanded',
647
- 'aria-labelledby',
648
- 'aria-describedby',
649
- ]);
646
+ const attributesToStore = ['aria-labelledby', 'aria-describedby'];
647
+ if (!isModal) {
648
+ attributesToStore.push('aria-expanded');
649
+ }
650
+ this.__storeOriginalAttrs(this.invokerNode, attributesToStore);
650
651
  }
651
652
 
652
653
  if (!this.contentNode.id) {
@@ -661,7 +662,7 @@ export class OverlayController extends EventTarget {
661
662
  }
662
663
  this.contentNode.setAttribute('role', 'tooltip');
663
664
  } else {
664
- if (this.invokerNode) {
665
+ if (this.invokerNode && !isModal) {
665
666
  this.invokerNode.setAttribute('aria-expanded', `${this.isShown}`);
666
667
  }
667
668
  if (!this.contentNode.getAttribute('role')) {
@@ -787,6 +788,10 @@ export class OverlayController extends EventTarget {
787
788
  * @protected
788
789
  */
789
790
  _keepBodySize({ phase }) {
791
+ if (!this.preventsScroll) {
792
+ return;
793
+ }
794
+
790
795
  switch (phase) {
791
796
  case 'before-show':
792
797
  this.__bodyClientWidth = document.body.clientWidth;
@@ -1299,7 +1304,8 @@ export class OverlayController extends EventTarget {
1299
1304
  if (phase === 'init' || phase === 'teardown') {
1300
1305
  this.__setupTeardownAccessibility({ phase });
1301
1306
  }
1302
- if (this.invokerNode && !this.isTooltip) {
1307
+ const isModal = this.trapsKeyboardFocus;
1308
+ if (this.invokerNode && !this.isTooltip && !isModal) {
1303
1309
  this.invokerNode.setAttribute('aria-expanded', `${phase === 'show'}`);
1304
1310
  }
1305
1311
  }
@@ -72,7 +72,11 @@ export const OverlayMixinImplementation = superclass =>
72
72
  requestUpdate(name, oldValue, options) {
73
73
  super.requestUpdate(name, oldValue, options);
74
74
  if (name === 'opened' && this.opened !== oldValue) {
75
- this.dispatchEvent(new Event('opened-changed'));
75
+ this.dispatchEvent(
76
+ new CustomEvent('opened-changed', {
77
+ detail: { opened: this.opened },
78
+ }),
79
+ );
76
80
  }
77
81
  }
78
82
 
@@ -1528,6 +1528,23 @@ describe('OverlayController', () => {
1528
1528
  expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal('false');
1529
1529
  });
1530
1530
 
1531
+ it('does not synchronize [aria-expanded] on invoker when the overlay is modal', async () => {
1532
+ const invokerNode = /** @type {HTMLElement} */ (
1533
+ await fixture('<div role="button">invoker</div>')
1534
+ );
1535
+ const ctrl = new OverlayController({
1536
+ ...withLocalTestConfig(),
1537
+ trapsKeyboardFocus: true,
1538
+ handlesAccessibility: true,
1539
+ invokerNode,
1540
+ });
1541
+ expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal(null);
1542
+ await ctrl.show();
1543
+ expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal(null);
1544
+ await ctrl.hide();
1545
+ expect(ctrl.invokerNode?.getAttribute('aria-expanded')).to.equal(null);
1546
+ });
1547
+
1531
1548
  it('creates unique id for content', async () => {
1532
1549
  const ctrl = new OverlayController({
1533
1550
  ...withLocalTestConfig(),
@@ -1764,4 +1781,33 @@ describe('OverlayController', () => {
1764
1781
  );
1765
1782
  });
1766
1783
  });
1784
+
1785
+ describe('run _keepBodySize only with scroll prevention', () => {
1786
+ /**
1787
+ * @type {OverlayController}
1788
+ */
1789
+ const overlayControllerNoPrevent = new OverlayController({
1790
+ ...withLocalTestConfig(),
1791
+ preventsScroll: false,
1792
+ });
1793
+
1794
+ const overlayControllerPreventsScroll = new OverlayController({
1795
+ ...withLocalTestConfig(),
1796
+ preventsScroll: true,
1797
+ });
1798
+
1799
+ it('should not run with scroll prevention', async () => {
1800
+ await overlayControllerNoPrevent.show();
1801
+
1802
+ expect(overlayControllerNoPrevent.__bodyMarginRightInline).to.equal(undefined);
1803
+ expect(overlayControllerNoPrevent.__bodyMarginRight).to.equal(undefined);
1804
+ });
1805
+
1806
+ it('should run with scroll prevention', async () => {
1807
+ await overlayControllerPreventsScroll.show();
1808
+
1809
+ expect(overlayControllerPreventsScroll.__bodyMarginRightInline).to.not.equal(undefined);
1810
+ expect(overlayControllerPreventsScroll.__bodyMarginRight).to.not.equal(undefined);
1811
+ });
1812
+ });
1767
1813
  });
@@ -196,6 +196,18 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
196
196
  }
197
197
  }
198
198
 
199
+ if (changedProperties.has('singleOption')) {
200
+ if (this.singleOption) {
201
+ this._invokerNode.removeAttribute('role');
202
+ this._invokerNode.removeAttribute('aria-haspopup');
203
+ this._invokerNode.removeAttribute('aria-expanded');
204
+ } else {
205
+ this._invokerNode.setAttribute('role', 'button');
206
+ this._invokerNode.setAttribute('aria-haspopup', 'listbox');
207
+ this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
208
+ }
209
+ }
210
+
199
211
  if (this._inputNode && this._invokerNode) {
200
212
  if (changedProperties.has('_ariaLabelledNodes')) {
201
213
  this._invokerNode.setAttribute(
@@ -148,6 +148,32 @@ describe('lion-select-rich', () => {
148
148
  expect(_invokerNode.hasAttribute('single-option')).to.be.true;
149
149
  });
150
150
 
151
+ it('sets and removes the button role and aria attributes on change of singleOption', async () => {
152
+ const el = await fixture(html`
153
+ <lion-select-rich>
154
+ <lion-option .choiceValue=${10}>Item 1</lion-option>
155
+ <lion-option .choiceValue=${20}>Item 2</lion-option>
156
+ </lion-select-rich>
157
+ `);
158
+ const { _invokerNode } = getSelectRichMembers(el);
159
+
160
+ expect(_invokerNode.hasAttribute('role')).to.be.true;
161
+ expect(_invokerNode.hasAttribute('aria-haspopup')).to.be.true;
162
+ expect(_invokerNode.hasAttribute('aria-expanded')).to.be.true;
163
+
164
+ el.singleOption = true;
165
+ await el.updateComplete;
166
+ expect(_invokerNode.hasAttribute('role')).to.be.false;
167
+ expect(_invokerNode.hasAttribute('aria-haspopup')).to.be.false;
168
+ expect(_invokerNode.hasAttribute('aria-expanded')).to.be.false;
169
+
170
+ el.singleOption = false;
171
+ await el.updateComplete;
172
+ expect(_invokerNode.hasAttribute('role')).to.be.true;
173
+ expect(_invokerNode.hasAttribute('aria-haspopup')).to.be.true;
174
+ expect(_invokerNode.hasAttribute('aria-expanded')).to.be.true;
175
+ });
176
+
151
177
  it('updates the invoker when the selected element is the same but the modelValue was updated asynchronously', async () => {
152
178
  const tagString = defineCE(
153
179
  class LionCustomOption extends LionOption {