@lion/ui 0.1.5 → 0.2.1

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 (153) hide show
  1. package/components/accordion/src/LionAccordion.js +2 -2
  2. package/components/accordion/test/lion-accordion.test.js +31 -0
  3. package/components/calendar/src/LionCalendar.js +13 -3
  4. package/components/combobox/src/LionCombobox.js +107 -14
  5. package/components/combobox/src/validators.js +18 -0
  6. package/components/combobox/test/lion-combobox.test.js +286 -184
  7. package/components/combobox/test/validators.test.js +62 -0
  8. package/components/combobox/test-helpers/combobox-helpers.js +125 -0
  9. package/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts +4 -3
  10. package/components/input-datepicker/src/LionInputDatepicker.js +9 -1
  11. package/components/input-iban/translations/de.js +1 -1
  12. package/components/listbox/src/ListboxMixin.js +6 -0
  13. package/components/listbox/test-suites/ListboxMixin.suite.js +15 -8
  14. package/components/overlays/src/OverlayController.js +27 -7
  15. package/components/overlays/test/OverlayController.test.js +16 -2
  16. package/components/switch/src/LionSwitch.js +0 -3
  17. package/components/switch/src/LionSwitchButton.js +14 -1
  18. package/components/switch/test/lion-switch-button.test.js +15 -1
  19. package/components/switch/test/lion-switch.test.js +21 -6
  20. package/components/validate-messages/src/loadDefaultFeedbackMessagesNoSideEffects.js +11 -8
  21. package/components/validate-messages/translations/bg.js +4 -0
  22. package/components/validate-messages/translations/cs.js +2 -0
  23. package/components/validate-messages/translations/de.js +4 -0
  24. package/components/validate-messages/translations/en.js +2 -0
  25. package/components/validate-messages/translations/es.js +4 -0
  26. package/components/validate-messages/translations/fr.js +4 -0
  27. package/components/validate-messages/translations/hu.js +2 -0
  28. package/components/validate-messages/translations/it.js +4 -0
  29. package/components/validate-messages/translations/nl.js +4 -0
  30. package/components/validate-messages/translations/pl.js +2 -0
  31. package/components/validate-messages/translations/ro.js +4 -0
  32. package/components/validate-messages/translations/ru.js +4 -0
  33. package/components/validate-messages/translations/sk.js +2 -0
  34. package/components/validate-messages/translations/uk.js +4 -0
  35. package/components/validate-messages/translations/zh.js +2 -0
  36. package/custom-elements.json +1365 -34
  37. package/dist-types/components/calendar/src/LionCalendar.d.ts +2 -0
  38. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  39. package/dist-types/components/combobox/src/LionCombobox.d.ts +37 -13
  40. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  41. package/dist-types/components/combobox/src/validators.d.ts +13 -0
  42. package/dist-types/components/combobox/src/validators.d.ts.map +1 -0
  43. package/dist-types/components/combobox/test/lion-combobox.test.d.ts +0 -2
  44. package/dist-types/components/combobox/test/lion-combobox.test.d.ts.map +1 -1
  45. package/dist-types/components/combobox/test/validators.test.d.ts +2 -0
  46. package/dist-types/components/combobox/test/validators.test.d.ts.map +1 -0
  47. package/dist-types/components/combobox/test-helpers/combobox-helpers.d.ts +41 -0
  48. package/dist-types/components/combobox/test-helpers/combobox-helpers.d.ts.map +1 -0
  49. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts +4 -3
  50. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts.map +1 -1
  51. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +4 -0
  52. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts.map +1 -1
  53. package/dist-types/components/listbox/src/ListboxMixin.d.ts.map +1 -1
  54. package/dist-types/components/listbox/test-suites/ListboxMixin.suite.d.ts.map +1 -1
  55. package/dist-types/components/overlays/src/OverlayController.d.ts +2 -0
  56. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  57. package/dist-types/components/switch/src/LionSwitch.d.ts +0 -1
  58. package/dist-types/components/switch/src/LionSwitch.d.ts.map +1 -1
  59. package/dist-types/components/switch/src/LionSwitchButton.d.ts +3 -0
  60. package/dist-types/components/switch/src/LionSwitchButton.d.ts.map +1 -1
  61. package/dist-types/components/switch/test/lion-switch.test.d.ts +1 -1
  62. package/dist-types/components/switch/test/lion-switch.test.d.ts.map +1 -1
  63. package/dist-types/components/validate-messages/src/loadDefaultFeedbackMessagesNoSideEffects.d.ts.map +1 -1
  64. package/dist-types/components/validate-messages/translations/bg-BG.d.ts +2 -0
  65. package/dist-types/components/validate-messages/translations/bg.d.ts +3 -0
  66. package/dist-types/components/validate-messages/translations/cs-CZ.d.ts +2 -0
  67. package/dist-types/components/validate-messages/translations/cs.d.ts +3 -0
  68. package/dist-types/components/validate-messages/translations/de-DE.d.ts +2 -0
  69. package/dist-types/components/validate-messages/translations/de.d.ts +3 -0
  70. package/dist-types/components/validate-messages/translations/en-AU.d.ts +2 -0
  71. package/dist-types/components/validate-messages/translations/en-GB.d.ts +2 -0
  72. package/dist-types/components/validate-messages/translations/en-PH.d.ts +2 -0
  73. package/dist-types/components/validate-messages/translations/en-US.d.ts +2 -0
  74. package/dist-types/components/validate-messages/translations/en.d.ts +3 -0
  75. package/dist-types/components/validate-messages/translations/es-ES.d.ts +2 -0
  76. package/dist-types/components/validate-messages/translations/es.d.ts +3 -0
  77. package/dist-types/components/validate-messages/translations/fr-BE.d.ts +2 -0
  78. package/dist-types/components/validate-messages/translations/fr-FR.d.ts +2 -0
  79. package/dist-types/components/validate-messages/translations/fr.d.ts +3 -0
  80. package/dist-types/components/validate-messages/translations/hu-HU.d.ts +2 -0
  81. package/dist-types/components/validate-messages/translations/hu.d.ts +3 -0
  82. package/dist-types/components/validate-messages/translations/it-IT.d.ts +2 -0
  83. package/dist-types/components/validate-messages/translations/it.d.ts +3 -0
  84. package/dist-types/components/validate-messages/translations/nl-BE.d.ts +2 -0
  85. package/dist-types/components/validate-messages/translations/nl-NL.d.ts +2 -0
  86. package/dist-types/components/validate-messages/translations/nl.d.ts +3 -0
  87. package/dist-types/components/validate-messages/translations/pl-PL.d.ts +2 -0
  88. package/dist-types/components/validate-messages/translations/pl.d.ts +3 -0
  89. package/dist-types/components/validate-messages/translations/ro-RO.d.ts +2 -0
  90. package/dist-types/components/validate-messages/translations/ro.d.ts +3 -0
  91. package/dist-types/components/validate-messages/translations/ru-RU.d.ts +2 -0
  92. package/dist-types/components/validate-messages/translations/ru.d.ts +3 -0
  93. package/dist-types/components/validate-messages/translations/sk-SK.d.ts +2 -0
  94. package/dist-types/components/validate-messages/translations/sk.d.ts +3 -0
  95. package/dist-types/components/validate-messages/translations/uk-UA.d.ts +2 -0
  96. package/dist-types/components/validate-messages/translations/uk.d.ts +3 -0
  97. package/dist-types/components/validate-messages/translations/zh.d.ts +3 -0
  98. package/dist-types/exports/combobox-test-helpers.d.ts +2 -0
  99. package/dist-types/exports/combobox-test-helpers.d.ts.map +1 -0
  100. package/dist-types/exports/combobox.d.ts +1 -0
  101. package/dist-types/tsconfig.tsbuildinfo +1 -1
  102. package/docs/components/checkbox-group/overview.md +2 -2
  103. package/docs/components/checkbox-group/use-cases.md +1 -0
  104. package/docs/components/collapsible/overview.md +1 -1
  105. package/docs/components/combobox/overview.md +12 -2
  106. package/docs/components/combobox/use-cases.md +36 -1
  107. package/docs/components/dialog/overview.md +1 -1
  108. package/docs/components/fieldset/overview.md +5 -5
  109. package/docs/components/form/overview.md +3 -3
  110. package/docs/components/icon/use-cases.md +1 -1
  111. package/docs/components/input/overview.md +1 -1
  112. package/docs/components/input-amount/overview.md +3 -3
  113. package/docs/components/input-date/overview.md +3 -3
  114. package/docs/components/input-datepicker/overview.md +5 -5
  115. package/docs/components/input-email/overview.md +2 -2
  116. package/docs/components/input-iban/overview.md +2 -2
  117. package/docs/components/input-range/overview.md +2 -2
  118. package/docs/components/input-stepper/overview.md +1 -1
  119. package/docs/components/input-tel/overview.md +1 -1
  120. package/docs/components/input-tel/use-cases.md +6 -6
  121. package/docs/components/input-tel-dropdown/overview.md +1 -1
  122. package/docs/components/radio-group/overview.md +3 -3
  123. package/docs/components/select-rich/overview.md +7 -10
  124. package/docs/components/select-rich/use-cases.md +54 -69
  125. package/docs/components/tabs/examples.md +1 -1
  126. package/docs/fundamentals/index.md +4 -4
  127. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  128. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  129. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  130. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  131. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  132. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  133. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  134. package/docs/fundamentals/systems/core/overview.md +1 -1
  135. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  136. package/docs/fundamentals/systems/form/model-value.md +1 -1
  137. package/docs/fundamentals/systems/form/overview.md +28 -28
  138. package/docs/fundamentals/systems/form/validate.md +1 -1
  139. package/docs/fundamentals/systems/localize/overview.md +4 -4
  140. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  141. package/docs/fundamentals/systems/overlays/overview.md +5 -10
  142. package/docs/fundamentals/systems/overlays/rationale.md +3 -6
  143. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  144. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  145. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  146. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  147. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  148. package/docs/guides/index.md +8 -8
  149. package/docs/guides/principles/index.md +5 -5
  150. package/docs/guides/principles/styling.md +1 -1
  151. package/exports/combobox-test-helpers.js +1 -0
  152. package/exports/combobox.js +1 -0
  153. package/package.json +1 -1
@@ -0,0 +1,125 @@
1
+ import { getListboxMembers } from '@lion/ui/listbox-test-helpers.js';
2
+
3
+ /**
4
+ * @typedef {import('@lion/ui/combobox.js').LionCombobox} LionCombobox
5
+ */
6
+
7
+ /**
8
+ * @param { LionCombobox } el
9
+ */
10
+ export function getComboboxMembers(el) {
11
+ const obj = getListboxMembers(el);
12
+ return {
13
+ ...obj,
14
+ ...{
15
+ // @ts-ignore [allow-protected] in test
16
+ _invokerNode: el._invokerNode,
17
+ // @ts-ignore [allow-protected] in test
18
+ _overlayCtrl: el._overlayCtrl,
19
+ // @ts-ignore [allow-protected] in test
20
+ _comboboxNode: el._comboboxNode,
21
+ // @ts-ignore [allow-protected] in test
22
+ _inputNode: el._inputNode,
23
+ // @ts-ignore [allow-protected] in test
24
+ _listboxNode: el._listboxNode,
25
+ // @ts-ignore [allow-protected] in test
26
+ _selectionDisplayNode: el._selectionDisplayNode,
27
+ // @ts-ignore [allow-protected] in test
28
+ _activeDescendantOwnerNode: el._activeDescendantOwnerNode,
29
+ // @ts-ignore [allow-protected] in test
30
+ _ariaVersion: el._ariaVersion,
31
+ },
32
+ };
33
+ }
34
+
35
+ /**
36
+ * @param {LionCombobox} el
37
+ * @param {string} value
38
+ */
39
+ // TODO: add keys that actually make sense...
40
+ export function mimicUserTyping(el, value) {
41
+ const { _inputNode } = getComboboxMembers(el);
42
+ _inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
43
+ // eslint-disable-next-line no-param-reassign
44
+ _inputNode.value = value;
45
+ _inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
46
+ _inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
47
+ _inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
48
+ }
49
+
50
+ /**
51
+ * @param {HTMLElement} el
52
+ * @param {string} key
53
+ */
54
+ export function mimicKeyPress(el, key) {
55
+ el.dispatchEvent(new KeyboardEvent('keydown', { key }));
56
+ el.dispatchEvent(new KeyboardEvent('keyup', { key }));
57
+ }
58
+
59
+ /**
60
+ * @param {LionCombobox} el
61
+ * @param {string[]} values
62
+ */
63
+ export async function mimicUserTypingAdvanced(el, values) {
64
+ const { _inputNode } = getComboboxMembers(el);
65
+ _inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
66
+
67
+ for (const key of values) {
68
+ // eslint-disable-next-line no-await-in-loop, no-loop-func
69
+ await new Promise(resolve => {
70
+ const hasSelection = _inputNode.selectionStart !== _inputNode.selectionEnd;
71
+
72
+ if (key === 'Backspace') {
73
+ if (hasSelection) {
74
+ _inputNode.value =
75
+ _inputNode.value.slice(
76
+ 0,
77
+ _inputNode.selectionStart ? _inputNode.selectionStart : undefined,
78
+ ) +
79
+ _inputNode.value.slice(
80
+ _inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
81
+ _inputNode.value.length,
82
+ );
83
+ } else {
84
+ _inputNode.value = _inputNode.value.slice(0, -1);
85
+ }
86
+ } else if (hasSelection) {
87
+ _inputNode.value =
88
+ _inputNode.value.slice(
89
+ 0,
90
+ _inputNode.selectionStart ? _inputNode.selectionStart : undefined,
91
+ ) +
92
+ key +
93
+ _inputNode.value.slice(
94
+ _inputNode.selectionEnd ? _inputNode.selectionEnd : undefined,
95
+ _inputNode.value.length,
96
+ );
97
+ } else {
98
+ _inputNode.value += key;
99
+ }
100
+
101
+ mimicKeyPress(_inputNode, key);
102
+ _inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
103
+
104
+ el.updateComplete.then(() => {
105
+ // @ts-ignore
106
+ resolve();
107
+ });
108
+ });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * @param {LionCombobox} el
114
+ */
115
+ export function getFilteredOptionValues(el) {
116
+ const options = el.formElements;
117
+ /**
118
+ * @param {{ style: { display: string; }; }} option
119
+ */
120
+ const filtered = options.filter(option => option.getAttribute('aria-hidden') !== 'true');
121
+ /**
122
+ * @param {{ value: any; }} option
123
+ */
124
+ return filtered.map(option => option.value);
125
+ }
@@ -1,9 +1,10 @@
1
1
  import { Constructor } from '@open-wc/dedupe-mixin';
2
2
  import { LitElement } from 'lit';
3
- import { FormControlHost } from '../FormControlMixinTypes.js';
4
3
  import { FormControl } from '../form-group/FormGroupMixinTypes.js';
5
- import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes.js';
4
+ import { FormControlHost } from '../FormControlMixinTypes.js';
6
5
  import { InteractionStateHost } from '../InteractionStateMixinTypes.js';
6
+ import { FormRegistrarHost } from '../registration/FormRegistrarMixinTypes.js';
7
+ import { ChoiceInputHost } from './ChoiceInputMixinTypes.js';
7
8
 
8
9
  export declare class ChoiceGroupHost {
9
10
  multipleChoice: boolean;
@@ -26,7 +27,7 @@ export declare class ChoiceGroupHost {
26
27
  protected _throwWhenInvalidChildModelValue(child: FormControlHost): void;
27
28
  protected _isEmpty(): void;
28
29
  protected _checkSingleChoiceElements(ev: Event): void;
29
- protected _getCheckedElements(): void;
30
+ protected _getCheckedElements(): ChoiceInputHost[];
30
31
  protected _setCheckedElements(value: any, check: boolean): void;
31
32
  protected _onBeforeRepropagateChildrenValues(ev: Event): void;
32
33
 
@@ -279,11 +279,19 @@ export class LionInputDatepicker extends ScopedElementsMixin(
279
279
  aria-label="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
280
280
  title="${this.msgLit('lion-input-datepicker:openDatepickerLabel')}"
281
281
  >
282
- 📅
282
+ ${this._invokerIconTemplate()}
283
283
  </button>
284
284
  `;
285
285
  }
286
286
 
287
+ /**
288
+ * Subclassers can replace this with their custom extension invoker icon
289
+ */
290
+ // eslint-disable-next-line class-methods-use-this
291
+ _invokerIconTemplate() {
292
+ return html`📅`;
293
+ }
294
+
287
295
  _setupOverlayCtrl() {
288
296
  super._setupOverlayCtrl();
289
297
 
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  error: {
3
- IsIBAN: 'Geben Sie ein gültiges {fieldName} ein.',
3
+ IsIBAN: 'Geben Sie eine gültige {fieldName} ein.',
4
4
  IsCountryIBAN:
5
5
  'Geben Sie eine gültige {params, select,\n' +
6
6
  'AT {Österreichisch}\n' +
@@ -635,6 +635,9 @@ const ListboxMixinImplementation = superclass =>
635
635
  }
636
636
  break;
637
637
  case 'ArrowLeft':
638
+ if (this._listboxReceivesNoFocus) {
639
+ return;
640
+ }
638
641
  ev.preventDefault();
639
642
  if (this.orientation === 'horizontal') {
640
643
  this.activeIndex = this._getPreviousEnabledOption(this.activeIndex);
@@ -647,6 +650,9 @@ const ListboxMixinImplementation = superclass =>
647
650
  }
648
651
  break;
649
652
  case 'ArrowRight':
653
+ if (this._listboxReceivesNoFocus) {
654
+ return;
655
+ }
650
656
  ev.preventDefault();
651
657
  if (this.orientation === 'horizontal') {
652
658
  this.activeIndex = this._getNextEnabledOption(this.activeIndex);
@@ -393,7 +393,7 @@ export function runListboxMixinSuite(customConfig = {}) {
393
393
  `);
394
394
  await el.updateComplete;
395
395
  await el.updateComplete; // need 2 awaits as overlay.show is an async function
396
-
396
+ // for more info about why we need the ignoreRules, see: https://lion-web.netlify.app/fundamentals/systems/overlays/rationale/#considerations
397
397
  await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
398
398
  });
399
399
 
@@ -405,6 +405,7 @@ export function runListboxMixinSuite(customConfig = {}) {
405
405
  <${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
406
406
  </${tag}>
407
407
  `);
408
+ // for more info about why we need the ignoreRules, see: https://lion-web.netlify.app/fundamentals/systems/overlays/rationale/#considerations
408
409
  await expect(el).to.be.accessible({ ignoredRules: ['aria-allowed-role'] });
409
410
  });
410
411
 
@@ -861,6 +862,7 @@ export function runListboxMixinSuite(customConfig = {}) {
861
862
  mimicKeyPress(_listboxNode, 'End');
862
863
  expect(el.activeIndex).to.equal(3);
863
864
  });
865
+
864
866
  it('navigates through open lists with [ArrowDown] [ArrowUp] keys activates the option', async () => {
865
867
  const el = /** @type {LionListbox} */ (
866
868
  await fixture(html`
@@ -936,7 +938,7 @@ export function runListboxMixinSuite(customConfig = {}) {
936
938
  it('uses [ArrowLeft] and [ArrowRight] keys when "horizontal"', async () => {
937
939
  const el = /** @type {LionListbox} */ (
938
940
  await fixture(html`
939
- <${tag} opened name="foo" orientation="horizontal" autocomplete="none" show-all-on-empty>
941
+ <${tag} ._listboxReceivesNoFocus="${false}" opened name="foo" orientation="horizontal" autocomplete="none" show-all-on-empty>
940
942
  <${optionTag} .choiceValue="${'Artichoke'}">Artichoke</${optionTag}>
941
943
  <${optionTag} .choiceValue="${'Chard'}">Chard</${optionTag}>
942
944
  </${tag}>
@@ -948,15 +950,12 @@ export function runListboxMixinSuite(customConfig = {}) {
948
950
 
949
951
  // Normalize for suite tests
950
952
  el.activeIndex = 0;
951
-
952
953
  await el.updateComplete;
953
954
 
954
955
  mimicKeyPress(_listboxNode, 'ArrowRight');
955
-
956
956
  expect(el.activeIndex).to.equal(1);
957
957
 
958
958
  mimicKeyPress(_listboxNode, 'ArrowLeft');
959
-
960
959
  expect(el.activeIndex).to.equal(0);
961
960
 
962
961
  // No response to vertical arrows...
@@ -965,7 +964,14 @@ export function runListboxMixinSuite(customConfig = {}) {
965
964
 
966
965
  el.activeIndex = 1;
967
966
  mimicKeyPress(_listboxNode, 'ArrowUp');
967
+ expect(el.activeIndex).to.equal(1);
968
968
 
969
+ // @ts-ignore allow protected member access in test
970
+ el._listboxReceivesNoFocus = true;
971
+ mimicKeyPress(_listboxNode, 'ArrowLeft');
972
+ expect(el.activeIndex).to.equal(1);
973
+
974
+ mimicKeyPress(_listboxNode, 'ArrowRight');
969
975
  expect(el.activeIndex).to.equal(1);
970
976
  });
971
977
 
@@ -1141,6 +1147,7 @@ export function runListboxMixinSuite(customConfig = {}) {
1141
1147
  expect(el.checkedIndex).to.equal(0);
1142
1148
  expectOnlyGivenOneOptionToBeChecked(options, 0);
1143
1149
  });
1150
+
1144
1151
  it('navigates through list with [ArrowLeft] [ArrowRight] keys when horizontal: activates and checks the option', async () => {
1145
1152
  /**
1146
1153
  * @param {LionOption[]} options
@@ -1157,7 +1164,7 @@ export function runListboxMixinSuite(customConfig = {}) {
1157
1164
  }
1158
1165
  const el = /** @type {LionListbox} */ (
1159
1166
  await fixture(html`
1160
- <${tag} opened selection-follows-focus orientation="horizontal" autocomplete="none" show-all-on-empty>
1167
+ <${tag} ._listboxReceivesNoFocus="${false}" opened selection-follows-focus orientation="horizontal" autocomplete="none" show-all-on-empty>
1161
1168
  <${optionTag} .choiceValue=${10}>Item 1</${optionTag}>
1162
1169
  <${optionTag} .choiceValue=${20}>Item 2</${optionTag}>
1163
1170
  <${optionTag} .choiceValue=${30}>Item 3</${optionTag}>
@@ -1453,7 +1460,7 @@ export function runListboxMixinSuite(customConfig = {}) {
1453
1460
  )}
1454
1461
  </${tag}>
1455
1462
  `);
1456
- el.checkedIndex = 0;
1463
+ el.setCheckedIndex(0);
1457
1464
  expect(el.modelValue).to.deep.equal({
1458
1465
  type: 'mastercard',
1459
1466
  label: 'Master Card',
@@ -1461,7 +1468,7 @@ export function runListboxMixinSuite(customConfig = {}) {
1461
1468
  active: true,
1462
1469
  });
1463
1470
 
1464
- el.checkedIndex = 1;
1471
+ el.setCheckedIndex(1);
1465
1472
  expect(el.modelValue).to.deep.equal({
1466
1473
  type: 'visacard',
1467
1474
  label: 'Visa Card',
@@ -124,6 +124,9 @@ export class OverlayController extends EventTarget {
124
124
  /** @private */
125
125
  this.__sharedConfig = config;
126
126
 
127
+ /** @private */
128
+ this.__activeElementRightBeforeHide = null;
129
+
127
130
  /** @type {OverlayConfig} */
128
131
  this.config = {};
129
132
 
@@ -570,12 +573,18 @@ export class OverlayController extends EventTarget {
570
573
  // We use a dialog for its visual capabilities: it renders to the top layer.
571
574
  // A11y will depend on the type of overlay and is arranged on contentNode level.
572
575
  // Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
576
+ //
577
+ // The role="dialog" is set on the contentNode (or another role), so role="none"
578
+ // is valid here, although AXE complains about this setup.
579
+ // For now we need to add `ignoredRules: ['aria-allowed-role']` in your AXE tests.
580
+ // see: https://lion-web.netlify.app/fundamentals/systems/overlays/rationale/#considerations
573
581
  wrappingDialogElement.setAttribute('role', 'none');
574
582
  wrappingDialogElement.setAttribute('data-overlay-outer-wrapper', '');
575
583
  // N.B. position: fixed is needed to escape out of 'overflow: hidden'
576
584
  // We give a high z-index for non-modal dialogs, so that we at least win from all siblings of our
577
585
  // parent stacking context
578
- wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex};`;
586
+ // padding reset so we don't get a weird dialog visual square showing up
587
+ wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex}; padding: 0;`;
579
588
  this.__wrappingDialogNode = wrappingDialogElement;
580
589
 
581
590
  /**
@@ -722,7 +731,7 @@ export class OverlayController extends EventTarget {
722
731
  const event = new CustomEvent('before-show', { cancelable: true });
723
732
  this.dispatchEvent(event);
724
733
  if (!event.defaultPrevented) {
725
- if (this.__wrappingDialogNode instanceof HTMLDialogElement) {
734
+ if ('HTMLDialogElement' in window && this.__wrappingDialogNode instanceof HTMLDialogElement) {
726
735
  this.__wrappingDialogNode.open = true;
727
736
  }
728
737
  // @ts-ignore
@@ -827,6 +836,14 @@ export class OverlayController extends EventTarget {
827
836
  this._hideResolve = resolve;
828
837
  });
829
838
 
839
+ // save the current activeElement so we know if the user set focus to another element than the invoker of the dialog
840
+ // while the dialog was open.
841
+ // We need this in the _restoreFocus method to determine if we should focus this.elementToFocusAfterHide when the
842
+ // dialog is closed or keep focus on the element that the user deliberately gave focus
843
+ this.__activeElementRightBeforeHide = /** @type {ShadowRoot} */ (
844
+ this.contentNode.getRootNode()
845
+ ).activeElement;
846
+
830
847
  if (this.manager) {
831
848
  this.manager.hide(this);
832
849
  }
@@ -844,9 +861,10 @@ export class OverlayController extends EventTarget {
844
861
  contentNode: this.contentNode,
845
862
  });
846
863
 
847
- if (this.__wrappingDialogNode instanceof HTMLDialogElement) {
864
+ if ('HTMLDialogElement' in window && this.__wrappingDialogNode instanceof HTMLDialogElement) {
848
865
  this.__wrappingDialogNode.close();
849
866
  }
867
+
850
868
  // @ts-ignore
851
869
  this.__wrappingDialogNode.style.display = 'none';
852
870
  this._handleFeatures({ phase: 'hide' });
@@ -906,18 +924,20 @@ export class OverlayController extends EventTarget {
906
924
  /** @protected */
907
925
  _restoreFocus() {
908
926
  // We only are allowed to move focus if we (still) 'own' the active element.
909
- // Otherwise we assume the 'outside world' has purposefully taken over
910
- const { activeElement } = /** @type {ShadowRoot} */ (this.contentNode.getRootNode());
927
+ // Otherwise, we assume the 'outside world' has purposefully taken over
911
928
  const weStillOwnActiveElement =
912
- activeElement instanceof HTMLElement && this.contentNode.contains(activeElement);
929
+ this.__activeElementRightBeforeHide instanceof HTMLElement &&
930
+ this.contentNode.contains(this.__activeElementRightBeforeHide);
931
+
913
932
  if (!weStillOwnActiveElement) {
914
933
  return;
915
934
  }
916
935
 
917
936
  if (this.elementToFocusAfterHide) {
918
937
  this.elementToFocusAfterHide.focus();
938
+ this.elementToFocusAfterHide.scrollIntoView({ block: 'center' });
919
939
  } else {
920
- activeElement.blur();
940
+ /** @type {HTMLElement} */ (this.__activeElementRightBeforeHide).blur();
921
941
  }
922
942
  }
923
943
 
@@ -21,7 +21,7 @@ import { createShadowHost } from '../test-helpers/createShadowHost.js';
21
21
  * @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
22
22
  */
23
23
 
24
- const wrappingDialogNodeStyle = 'display: none; z-index: 9999;';
24
+ const wrappingDialogNodeStyle = 'display: none; z-index: 9999; padding: 0px;';
25
25
 
26
26
  /**
27
27
  * Make sure that all browsers serialize html in a similar way
@@ -46,6 +46,19 @@ function getProtectedMembers(overlayControllerEl) {
46
46
  };
47
47
  }
48
48
 
49
+ /**
50
+ * @param {HTMLElement} element
51
+ * */
52
+ const isInViewport = element => {
53
+ const rect = element.getBoundingClientRect();
54
+ return (
55
+ rect.top >= 0 &&
56
+ rect.left >= 0 &&
57
+ rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
58
+ rect.right <= (window.innerWidth || document.documentElement.clientWidth)
59
+ );
60
+ };
61
+
49
62
  const withGlobalTestConfig = () =>
50
63
  /** @type {OverlayConfig} */ ({
51
64
  placementMode: 'global',
@@ -905,9 +918,10 @@ describe('OverlayController', () => {
905
918
 
906
919
  await ctrl.hide();
907
920
  expect(document.activeElement).to.equal(input);
921
+ expect(isInViewport(input)).to.be.true;
908
922
  });
909
923
 
910
- it('supports elementToFocusAfterHide option when shadowRoot involved involved', async () => {
924
+ it('supports elementToFocusAfterHide option when shadowRoot involved', async () => {
911
925
  const input = /** @type {HTMLElement} */ (await fixture('<input />'));
912
926
  const contentNode = /** @type {HTMLElement} */ (
913
927
  await fixture('<div><textarea></textarea></div>')
@@ -80,9 +80,6 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
80
80
 
81
81
  connectedCallback() {
82
82
  super.connectedCallback();
83
- if (!this.role) {
84
- this.role = 'switch';
85
- }
86
83
  this.addEventListener('checked-changed', this.__handleButtonSwitchCheckedChanged);
87
84
  if (this._labelNode) {
88
85
  this._labelNode.addEventListener('click', this._toggleChecked);
@@ -78,6 +78,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
78
78
 
79
79
  this.role = 'switch';
80
80
  this.checked = false;
81
+ this.__initialized = false;
81
82
  /** @protected */
82
83
  this._toggleChecked = this._toggleChecked.bind(this);
83
84
  /** @private */
@@ -158,8 +159,20 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
158
159
  */
159
160
  requestUpdate(name, oldValue, options) {
160
161
  super.requestUpdate(name, oldValue, options);
161
- if (this.isConnected && name === 'checked' && this.checked !== oldValue && !this.disabled) {
162
+ if (
163
+ this.__initialized &&
164
+ this.isConnected &&
165
+ name === 'checked' &&
166
+ this.checked !== oldValue &&
167
+ !this.disabled
168
+ ) {
162
169
  this.__checkedStateChange();
163
170
  }
164
171
  }
172
+
173
+ /** @param {import('lit').PropertyValues } changedProperties */
174
+ firstUpdated(changedProperties) {
175
+ super.firstUpdated(changedProperties);
176
+ this.__initialized = true;
177
+ }
165
178
  }
@@ -1,4 +1,4 @@
1
- import { expect, fixture as _fixture } from '@open-wc/testing';
1
+ import { expect, fixture as _fixture, fixtureSync, elementUpdated } from '@open-wc/testing';
2
2
  import { html } from 'lit/static-html.js';
3
3
  import sinon from 'sinon';
4
4
  import '@lion/ui/define/lion-switch-button.js';
@@ -126,6 +126,20 @@ describe('lion-switch-button', () => {
126
126
  expect(handlerSpy.called).to.be.false;
127
127
  });
128
128
 
129
+ it('should not dispatch "checked-changed" event if not initialized on update', async () => {
130
+ const handlerSpy = sinon.spy();
131
+ const elSync = /** @type {LionSwitchButton} */ (
132
+ fixtureSync(html`<lion-switch-button @checked-changed="${handlerSpy}" .checked=${true} />`)
133
+ );
134
+ expect(elSync.__initialized).to.be.false;
135
+ await elementUpdated(elSync);
136
+ expect(elSync.__initialized).to.be.true;
137
+ expect(handlerSpy.called).to.be.false;
138
+ elSync.checked = !elSync.checked;
139
+ await elementUpdated(elSync);
140
+ expect(handlerSpy.called).to.be.true;
141
+ });
142
+
129
143
  describe('a11y', () => {
130
144
  it('should manage "aria-checked"', async () => {
131
145
  expect(el.hasAttribute('aria-checked')).to.be.true;
@@ -10,7 +10,7 @@ import '@lion/ui/define/lion-switch.js';
10
10
  /**
11
11
  * @typedef {import('../src/LionSwitchButton.js').LionSwitchButton} LionSwitchButton
12
12
  * @typedef {import('lit').TemplateResult} TemplateResult
13
- * @typedef {import('../../form-core/types/FormControlMixinTypes.js').FormControlHost} FormControlHost
13
+ * @typedef {import('@lion/ui/types/form-core.js').FormControlHost} FormControlHost
14
14
  */
15
15
 
16
16
  const IsTrue = class extends Validator {
@@ -215,10 +215,25 @@ describe('lion-switch', () => {
215
215
  expect(el.showsFeedbackFor).to.eql(['info']);
216
216
  });
217
217
 
218
- it('should not cause error by setting an attribute in the constructor', async () => {
219
- const div = await fixture(html`<div></div>`);
220
- const el = /** @type {LionSwitch} */ (document.createElement('lion-switch'));
221
- div.appendChild(el);
222
- expect(el.role).to.equal('switch');
218
+ describe('Accessibility', () => {
219
+ it('passes axe a11y audit', async () => {
220
+ const el = await fixture(html`<lion-switch name="hi" label="My label"></lion-switch>`);
221
+ await expect(el).to.be.accessible();
222
+
223
+ el.checked = true;
224
+ await el.updateComplete;
225
+ await expect(el).to.be.accessible();
226
+ });
227
+
228
+ it('passes axe a11y audit when disabled', async () => {
229
+ const el = await fixture(
230
+ html`<lion-switch name="hi" label="My label" disabled></lion-switch>`,
231
+ );
232
+ await expect(el).to.be.accessible();
233
+
234
+ el.checked = true;
235
+ await el.updateComplete;
236
+ await expect(el).to.be.accessible();
237
+ });
223
238
  });
224
239
  });
@@ -1,22 +1,23 @@
1
1
  /* eslint-disable import/no-extraneous-dependencies */
2
+ import { MatchesOption } from '@lion/ui/combobox.js';
2
3
  import {
3
4
  DefaultSuccess,
5
+ EqualsLength,
4
6
  IsDate,
5
7
  IsDateDisabled,
8
+ IsEmail,
9
+ IsNumber,
6
10
  MaxDate,
11
+ MaxLength,
12
+ MaxNumber,
7
13
  MinDate,
14
+ MinLength,
8
15
  MinMaxDate,
9
- IsNumber,
10
- MaxNumber,
16
+ MinMaxLength,
11
17
  MinMaxNumber,
12
18
  MinNumber,
13
- Required,
14
- EqualsLength,
15
- IsEmail,
16
- MaxLength,
17
- MinLength,
18
- MinMaxLength,
19
19
  Pattern,
20
+ Required,
20
21
  } from '@lion/ui/form-core.js';
21
22
  import { PhoneNumber } from '@lion/ui/input-tel.js';
22
23
 
@@ -162,6 +163,8 @@ export function loadDefaultFeedbackMessagesNoSideEffects({ localize }) {
162
163
  MinMaxDate.getMessage = async data => getLocalizedMessage(data);
163
164
  /** @param {FeedbackMessageData} data */
164
165
  IsDateDisabled.getMessage = async data => getLocalizedMessage(data);
166
+ /** @param {FeedbackMessageData} data */
167
+ MatchesOption.getMessage = async data => getLocalizedMessage(data);
165
168
 
166
169
  DefaultSuccess.getMessage = async data => {
167
170
  await forMessagesToBeReady();
@@ -17,6 +17,8 @@ export default {
17
17
  'Моля, въведете {fieldName} между {params.min, date, YYYYMMDD} и {params.max, date, YYYYMMDD}.',
18
18
  IsDateDisabled: 'Тази дата не е на разположение, моля, изберете друга.',
19
19
  IsEmail: 'Моля, въведете валиден {fieldName} с формат "name@example.com".',
20
+ MatchesOption:
21
+ 'Не са открити съответстващи резултати. Моля, опитайте с друга ключова дума или категория.',
20
22
  },
21
23
  warning: {
22
24
  Required: 'Моля, въведете също {fieldName}.',
@@ -35,6 +37,8 @@ export default {
35
37
  'Моля, въведете {fieldName} между {params.min, date, YYYYMMDD} и {params.max, date, YYYYMMDD}.',
36
38
  IsDateDisabled: 'Тази дата не е на разположение, моля, изберете друга.',
37
39
  IsEmail: 'Моля, въведете валиден {fieldName} с формат "name@example.com".',
40
+ MatchesOption:
41
+ 'Не са открити съответстващи резултати. Моля, опитайте с друга ключова дума или категория.',
38
42
  },
39
43
  success: {
40
44
  DefaultOk: 'Добре',
@@ -17,6 +17,7 @@ export default {
17
17
  'Zadejte {fieldName} od {params.min, date, YYYYMMDD} do {params.max, date, YYYYMMDD}.',
18
18
  IsDateDisabled: 'Toto datum je nedostupné, vyberte jiné.',
19
19
  IsEmail: 'Zadejte platný {fieldName} ve formátu "name@example.com".',
20
+ MatchesOption: 'Žádné odpovídající výsledky. Zkuste jiné klíčové slovo nebo kategorii.',
20
21
  },
21
22
  warning: {
22
23
  Required: 'Zadejte rovněž {fieldName}.',
@@ -35,6 +36,7 @@ export default {
35
36
  'Zadejte {fieldName} od {params.min, date, YYYYMMDD} do {params.max, date, YYYYMMDD}.',
36
37
  IsDateDisabled: 'Toto datum je nedostupné, vyberte jiné.',
37
38
  IsEmail: 'Zadejte platný {fieldName} ve formátu "name@example.com".',
39
+ MatchesOption: 'Žádné odpovídající výsledky. Zkuste jiné klíčové slovo nebo kategorii.',
38
40
  },
39
41
  success: {
40
42
  DefaultOk: 'Dobře',
@@ -18,6 +18,8 @@ export default {
18
18
  'Geben Sie für {fieldName} einen Wert zwischen {params.min, date, YYYYMMDD} und {params.max, date, YYYYMMDD} ein.',
19
19
  IsDateDisabled: 'Dieses Datum ist nicht verfügbar, bitte wählen Sie ein anderes Datum.',
20
20
  IsEmail: 'Geben Sie einen gültige {fieldName} im Format „name@example.com“ ein.',
21
+ MatchesOption:
22
+ 'Keine übereinstimmenden Ergebnisse. Bitte versuchen Sie es mit einem anderen Schlüsselbegriff oder einer anderen Kategorie.',
21
23
  },
22
24
  warning: {
23
25
  Required: '{fieldName} sollte ausgefüllt werden.',
@@ -37,6 +39,8 @@ export default {
37
39
  'Geben Sie für {fieldName} einen Wert zwischen {params.min, date, YYYYMMDD} und {params.max, date, YYYYMMDD} ein.',
38
40
  IsDateDisabled: 'Dieses Datum ist nicht verfügbar, bitte wählen Sie ein anderes Datum.',
39
41
  IsEmail: 'Geben Sie einen gültige {fieldName} im Format „name@example.com“ ein.',
42
+ MatchesOption:
43
+ 'Keine übereinstimmenden Ergebnisse. Bitte versuchen Sie es mit einem anderen Schlüsselbegriff oder einer anderen Kategorie.',
40
44
  },
41
45
  success: {
42
46
  DefaultOk: 'OK',
@@ -18,6 +18,7 @@ export default {
18
18
  'Please enter a {fieldName} between {params.min, date, YYYYMMDD} and {params.max, date, YYYYMMDD}.',
19
19
  IsDateDisabled: 'This date is unavailable, please choose another one.',
20
20
  IsEmail: 'Please enter a valid {fieldName} in the format "name@example.com".',
21
+ MatchesOption: 'No matching results. Please try a different keyword or category.',
21
22
  },
22
23
  warning: {
23
24
  Required: 'Please enter a(n) {fieldName}.',
@@ -37,6 +38,7 @@ export default {
37
38
  'Please enter a {fieldName} between {params.min, date, YYYYMMDD} and {params.max, date, YYYYMMDD}.',
38
39
  IsDateDisabled: 'This date is unavailable, please choose another one.',
39
40
  IsEmail: 'Please enter a valid {fieldName} in the format "name@example.com".',
41
+ MatchesOption: 'No matching results. Please try a different keyword or category.',
40
42
  },
41
43
  success: {
42
44
  DefaultOk: 'Okay',
@@ -18,6 +18,8 @@ export default {
18
18
  'Introduzca un/a {fieldName} entre {params.min, date, YYYYMMDD} y {params.max, date, YYYYMMDD}.',
19
19
  IsDateDisabled: 'Esta fecha no está disponible. Elija otra.',
20
20
  IsEmail: 'Introduzca un/a {fieldName} válido/a con el formato "nombre@ejemplo.com".',
21
+ MatchesOption:
22
+ 'No hay resultados que coincidan. Pruebe con una palabra clave o categoría diferente.',
21
23
  },
22
24
  warning: {
23
25
  Required: 'Introduzca también un/a {fieldName}.',
@@ -37,6 +39,8 @@ export default {
37
39
  'Introduzca un/a {fieldName} entre {params.min, date, YYYYMMDD} y {params.max, date, YYYYMMDD}.',
38
40
  IsDateDisabled: 'Esta fecha no está disponible. Elija otra.',
39
41
  IsEmail: 'Introduzca un/a {fieldName} válido/a con el formato "nombre@ejemplo.com".',
42
+ MatchesOption:
43
+ 'No hay resultados que coincidan. Pruebe con una palabra clave o categoría diferente.',
40
44
  },
41
45
  success: {
42
46
  DefaultOk: 'Vale',