@lion/ui 0.8.8 → 0.9.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 (105) hide show
  1. package/components/combobox/src/LionCombobox.js +6 -2
  2. package/components/combobox/test/lion-combobox.test.js +56 -3
  3. package/components/core/src/DisabledMixin.js +6 -0
  4. package/components/core/test/DisabledMixin.test.js +22 -1
  5. package/components/core/test/SlotMixin.test.js +3 -3
  6. package/components/form-core/test/FocusMixin.test.js +6 -4
  7. package/components/form-core/test-suites/NativeTextFieldMixin.suite.js +4 -8
  8. package/components/form-integrations/test/form-integrations.test.js +2 -2
  9. package/components/input-datepicker/translations/es.js +1 -1
  10. package/components/input-stepper/src/LionInputStepper.js +110 -35
  11. package/components/input-stepper/test/lion-input-stepper.test.js +96 -1
  12. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +7 -6
  13. package/components/input-tel-dropdown/test/LionInputTelDropdown.test.js +62 -10
  14. package/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.js +16 -12
  15. package/components/overlays/src/OverlayController.js +9 -0
  16. package/components/overlays/test/OverlayController.test.js +27 -3
  17. package/components/select-rich/test/lion-select-rich.test.js +9 -8
  18. package/components/switch/test/lion-switch.test.js +1 -1
  19. package/components/tabs/test/lion-tabs.test.js +12 -5
  20. package/custom-elements.json +506 -69
  21. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  22. package/dist-types/components/core/src/DisabledMixin.d.ts.map +1 -1
  23. package/dist-types/components/form-core/test-suites/NativeTextFieldMixin.suite.d.ts.map +1 -1
  24. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +33 -24
  25. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  26. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts.map +1 -1
  27. package/dist-types/components/input-tel-dropdown/test/LionInputTelDropdown.test.d.ts +2 -2
  28. package/dist-types/components/input-tel-dropdown/test/LionInputTelDropdown.test.d.ts.map +1 -1
  29. package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.d.ts +2 -2
  30. package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.d.ts.map +1 -1
  31. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  32. package/dist-types/components/tabs/test/lion-tabs.test.d.ts.map +1 -1
  33. package/dist-types/tsconfig.tsbuildinfo +1 -1
  34. package/docs/components/button/api-table.md +3 -0
  35. package/docs/components/checkbox-group/api-table.md +3 -0
  36. package/docs/components/checkbox-group/overview.md +2 -2
  37. package/docs/components/collapsible/overview.md +1 -1
  38. package/docs/components/combobox/api-table.md +1 -0
  39. package/docs/components/combobox/overview.md +1 -1
  40. package/docs/components/combobox/use-cases.md +1 -1
  41. package/docs/components/dialog/overview.md +1 -1
  42. package/docs/components/fieldset/api-table.md +1 -0
  43. package/docs/components/fieldset/overview.md +5 -5
  44. package/docs/components/form/overview.md +3 -3
  45. package/docs/components/form/use-cases.md +2 -2
  46. package/docs/components/icon/use-cases.md +1 -1
  47. package/docs/components/input/api-table.md +1 -0
  48. package/docs/components/input/overview.md +1 -1
  49. package/docs/components/input-amount/api-table.md +1 -0
  50. package/docs/components/input-amount/overview.md +3 -3
  51. package/docs/components/input-date/api-table.md +1 -0
  52. package/docs/components/input-date/overview.md +3 -3
  53. package/docs/components/input-datepicker/api-table.md +1 -0
  54. package/docs/components/input-datepicker/overview.md +5 -5
  55. package/docs/components/input-email/api-table.md +1 -0
  56. package/docs/components/input-email/overview.md +2 -2
  57. package/docs/components/input-file/api-table.md +1 -0
  58. package/docs/components/input-file/overview.md +1 -1
  59. package/docs/components/input-iban/api-table.md +1 -0
  60. package/docs/components/input-iban/overview.md +2 -2
  61. package/docs/components/input-range/api-table.md +1 -0
  62. package/docs/components/input-range/overview.md +2 -2
  63. package/docs/components/input-stepper/api-table.md +7 -3
  64. package/docs/components/input-stepper/overview.md +1 -1
  65. package/docs/components/input-stepper/use-cases.md +30 -0
  66. package/docs/components/input-tel/api-table.md +1 -0
  67. package/docs/components/input-tel/overview.md +1 -1
  68. package/docs/components/input-tel/use-cases.md +6 -6
  69. package/docs/components/input-tel-dropdown/api-table.md +1 -0
  70. package/docs/components/input-tel-dropdown/overview.md +1 -1
  71. package/docs/components/listbox/api-table.md +2 -0
  72. package/docs/components/listbox/use-cases.md +1 -1
  73. package/docs/components/radio-group/api-table.md +2 -0
  74. package/docs/components/radio-group/overview.md +3 -3
  75. package/docs/components/select/api-table.md +1 -0
  76. package/docs/components/select/use-cases.md +1 -1
  77. package/docs/components/select-rich/api-table.md +2 -0
  78. package/docs/components/select-rich/use-cases.md +1 -1
  79. package/docs/components/switch/api-table.md +2 -0
  80. package/docs/components/tabs/examples.md +1 -1
  81. package/docs/components/textarea/api-table.md +1 -0
  82. package/docs/fundamentals/index.md +4 -4
  83. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  84. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  85. package/docs/fundamentals/rationales/accessibility.md +1 -1
  86. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  87. package/docs/fundamentals/systems/core/overview.md +1 -1
  88. package/docs/fundamentals/systems/form/api-table.md +1 -0
  89. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  90. package/docs/fundamentals/systems/form/model-value.md +1 -1
  91. package/docs/fundamentals/systems/form/overview.md +28 -28
  92. package/docs/fundamentals/systems/form/validate.md +2 -2
  93. package/docs/fundamentals/systems/localize/overview.md +4 -4
  94. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  95. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  96. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  97. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  98. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  99. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  100. package/docs/guides/how-to/create-a-form.md +4 -4
  101. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  102. package/docs/guides/index.md +8 -8
  103. package/docs/guides/principles/index.md +5 -5
  104. package/docs/guides/principles/styling.md +1 -1
  105. package/package.json +1 -1
@@ -663,7 +663,11 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
663
663
  ) {
664
664
  return false;
665
665
  }
666
- if (this.filled || this.showAllOnEmpty) {
666
+ if (
667
+ this.filled ||
668
+ this.showAllOnEmpty ||
669
+ (!this.filled && this.multipleChoice && this.__prevCboxValueNonSelected)
670
+ ) {
667
671
  return true;
668
672
  }
669
673
  // when no keyboard action involved (on focused change), return current opened state
@@ -705,7 +709,7 @@ export class LionCombobox extends LocalizeMixin(OverlayMixin(CustomChoiceGroupMi
705
709
  // eslint-disable-next-line no-unused-vars
706
710
  _textboxOnInput(ev) {
707
711
  this.__shouldAutocompleteNextUpdate = true;
708
- this.opened = true;
712
+ this.opened = this._showOverlayCondition({});
709
713
  }
710
714
 
711
715
  /**
@@ -931,6 +931,50 @@ describe('lion-combobox', () => {
931
931
  expect(el.opened).to.equal(false);
932
932
  });
933
933
 
934
+ it('does not flash the menu when _showOverlayCondition returns "false"', async () => {
935
+ class ComplexCombobox extends LionCombobox {
936
+ _showOverlayCondition() {
937
+ return false;
938
+ }
939
+ }
940
+
941
+ const tagName = defineCE(ComplexCombobox);
942
+ const tag = unsafeStatic(tagName);
943
+
944
+ const el = /** @type {LionCombobox} */ (
945
+ await fixture(html`
946
+ <${tag} name="combo" label="Display only the label once selected">
947
+ <lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
948
+ <lion-option .choiceValue="${'Chard'}">Chard</lion-option>
949
+ <lion-option .choiceValue="${'Chicory'}">Chicory</lion-option>
950
+ <lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
951
+ </${tag}>
952
+ `)
953
+ );
954
+
955
+ const dialog = el.shadowRoot?.querySelector('dialog');
956
+ /**
957
+ * hasDropdownFlashed is `true` if the menu was shown for a short period of time and then got closed
958
+ */
959
+ let hasDropdownFlashed = false;
960
+ const observer = new MutationObserver(mutationList => {
961
+ // eslint-disable-next-line no-unused-vars
962
+ for (const mutation of mutationList) {
963
+ if (dialog?.style.display === '') {
964
+ hasDropdownFlashed = true;
965
+ }
966
+ }
967
+ });
968
+ observer.observe(/** @type {Node} */ (dialog), { attributeFilter: ['style'] });
969
+
970
+ const { _inputNode } = getComboboxMembers(el);
971
+ _inputNode.focus();
972
+ await sendKeys({
973
+ type: 'art',
974
+ });
975
+ expect(hasDropdownFlashed).to.be.false;
976
+ });
977
+
934
978
  it('shows overlay on click when filled', async () => {
935
979
  const el = /** @type {LionCombobox} */ (
936
980
  await fixture(html`
@@ -1157,7 +1201,10 @@ describe('lion-combobox', () => {
1157
1201
  class ShowOverlayConditionCombobox extends LionCombobox {
1158
1202
  /** @param {{ currentValue: string, lastKey:string }} options */
1159
1203
  _showOverlayCondition(options) {
1160
- return options.currentValue.length > 3 && super._showOverlayCondition(options);
1204
+ return (
1205
+ // @ts-ignore
1206
+ this.__prevCboxValueNonSelected.length > 3 && super._showOverlayCondition(options)
1207
+ );
1161
1208
  }
1162
1209
  }
1163
1210
  const tagName = defineCE(ShowOverlayConditionCombobox);
@@ -1182,7 +1229,10 @@ describe('lion-combobox', () => {
1182
1229
  class ShowOverlayConditionCombobox extends LionCombobox {
1183
1230
  /** @param {{ currentValue: string, lastKey:string }} options */
1184
1231
  _showOverlayCondition(options) {
1185
- return options.currentValue.length > 3 && super._showOverlayCondition(options);
1232
+ return (
1233
+ // @ts-ignore
1234
+ this.__prevCboxValueNonSelected.length > 3 && super._showOverlayCondition(options)
1235
+ );
1186
1236
  }
1187
1237
  }
1188
1238
  const tagName = defineCE(ShowOverlayConditionCombobox);
@@ -1207,7 +1257,10 @@ describe('lion-combobox', () => {
1207
1257
  class ShowOverlayConditionCombobox extends LionCombobox {
1208
1258
  /** @param {{ currentValue: string, lastKey:string }} options */
1209
1259
  _showOverlayCondition(options) {
1210
- return options.currentValue.length > 3 && super._showOverlayCondition(options);
1260
+ return (
1261
+ // @ts-ignore
1262
+ this.__prevCboxValueNonSelected.length > 3 && super._showOverlayCondition(options)
1263
+ );
1211
1264
  }
1212
1265
  }
1213
1266
  const tagName = defineCE(ShowOverlayConditionCombobox);
@@ -74,6 +74,12 @@ const DisabledMixinImplementation = superclass =>
74
74
  }
75
75
  }
76
76
  }
77
+
78
+ click() {
79
+ if (this.disabled) return;
80
+
81
+ super.click();
82
+ }
77
83
  };
78
84
 
79
85
  export const DisabledMixin = dedupeMixin(DisabledMixinImplementation);
@@ -1,7 +1,8 @@
1
1
  import { expect, fixture } from '@open-wc/testing';
2
+ import { DisabledMixin } from '@lion/ui/core.js';
2
3
  import { html } from 'lit/static-html.js';
3
4
  import { LitElement } from 'lit';
4
- import { DisabledMixin } from '@lion/ui/core.js';
5
+ import sinon from 'sinon';
5
6
 
6
7
  describe('DisabledMixin', () => {
7
8
  class CanBeDisabled extends DisabledMixin(LitElement) {}
@@ -84,4 +85,24 @@ describe('DisabledMixin', () => {
84
85
  el.retractRequestToBeDisabled();
85
86
  expect(el.disabled).to.be.false;
86
87
  });
88
+
89
+ it('will not perform click when disabled', async () => {
90
+ const el = /** @type {CanBeDisabled} */ (
91
+ await fixture(html`<can-be-disabled></can-be-disabled>`)
92
+ );
93
+ const spy = sinon.spy();
94
+ el.addEventListener('click', spy);
95
+
96
+ el.click();
97
+
98
+ expect(spy.callCount).to.equal(1);
99
+
100
+ el.disabled = true;
101
+ el.click();
102
+ expect(spy.callCount).to.equal(1);
103
+
104
+ el.disabled = false;
105
+ el.click();
106
+ expect(spy.callCount).to.equal(2);
107
+ });
87
108
  });
@@ -273,13 +273,13 @@ describe('SlotMixin', () => {
273
273
 
274
274
  el._focusableNode._buttonNode.focus();
275
275
 
276
- expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
276
+ expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
277
277
 
278
278
  el.currentValue = 1;
279
279
  await el.updateComplete;
280
280
 
281
281
  expect(isActiveElement(el._focusableNode)).to.be.true;
282
- expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
282
+ expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
283
283
  });
284
284
 
285
285
  it('allows for rerendering complex shadow root into slot as a direct child', async () => {
@@ -342,7 +342,7 @@ describe('SlotMixin', () => {
342
342
  await el.updateComplete;
343
343
 
344
344
  expect(isActiveElement(el._focusableNode)).to.be.true;
345
- expect(el._focusableNode.shadowRoot.activeElement).to.equal(el._focusableNode._buttonNode);
345
+ expect(isActiveElement(el._focusableNode._buttonNode, { deep: true })).to.be.true;
346
346
  });
347
347
 
348
348
  describe('firstRenderOnConnected (for backwards compatibility)', () => {
@@ -1,7 +1,9 @@
1
- import { LitElement } from 'lit';
2
1
  import { defineCE, expect, fixture, html, oneEvent, unsafeStatic } from '@open-wc/testing';
3
- import sinon from 'sinon';
4
2
  import { FocusMixin } from '@lion/ui/form-core.js';
3
+ import { LitElement } from 'lit';
4
+ import sinon from 'sinon';
5
+
6
+ import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
5
7
 
6
8
  const windowWithOptionalPolyfill =
7
9
  /** @type {Window & typeof globalThis & {applyFocusVisiblePolyfill?: function}} */ (window);
@@ -84,9 +86,9 @@ describe('FocusMixin', () => {
84
86
  const { _focusableNode } = el;
85
87
 
86
88
  el.focus();
87
- expect(document.activeElement === _focusableNode).to.be.true;
89
+ expect(isActiveElement(_focusableNode)).to.be.true;
88
90
  el.blur();
89
- expect(document.activeElement === _focusableNode).to.be.false;
91
+ expect(isActiveElement(_focusableNode)).to.be.false;
90
92
  });
91
93
 
92
94
  it('has an attribute focused when focused', async () => {
@@ -51,7 +51,7 @@ export function runNativeTextFieldMixinSuite(customConfig) {
51
51
  expect(_inputNode.selectionEnd).to.equal(2);
52
52
  });
53
53
 
54
- it('move focus to a next focusable element after writing some text', async () => {
54
+ it('moves focus to a next focusable element after writing some text', async () => {
55
55
  const el = /** @type {NativeTextFieldClass} */ (await fixture(html`<${tag}></${tag}>`));
56
56
  // @ts-ignore [allow-protected] in test
57
57
  const setValueAndPreserveCaretSpy = spy(el, '_setValueAndPreserveCaret');
@@ -59,12 +59,8 @@ export function runNativeTextFieldMixinSuite(customConfig) {
59
59
  await triggerFocusFor(el);
60
60
  await el.updateComplete;
61
61
  expect(isActiveElement(_inputNode)).to.be.true;
62
- await sendKeys({
63
- press: 'h',
64
- });
65
- await sendKeys({
66
- press: 'Tab',
67
- });
62
+ await sendKeys({ press: 'h' });
63
+ await sendKeys({ press: 'Tab' });
68
64
 
69
65
  expect(setValueAndPreserveCaretSpy.calledOnce).to.be.false;
70
66
 
@@ -73,7 +69,7 @@ export function runNativeTextFieldMixinSuite(customConfig) {
73
69
  return;
74
70
  }
75
71
 
76
- expect(document.activeElement).to.not.equal(_inputNode);
72
+ expect(isActiveElement(_inputNode)).to.be.false;
77
73
  });
78
74
  });
79
75
  }
@@ -48,7 +48,7 @@ describe('Form Integrations', () => {
48
48
  notifications: { value: '', checked: false },
49
49
  rsvp: '',
50
50
  tel: '',
51
- 'tel-dropdown': '+44',
51
+ 'tel-dropdown': '',
52
52
  comments: '',
53
53
  });
54
54
  });
@@ -82,7 +82,7 @@ describe('Form Integrations', () => {
82
82
  notifications: '',
83
83
  rsvp: '',
84
84
  tel: '',
85
- 'tel-dropdown': '+44',
85
+ 'tel-dropdown': '',
86
86
  comments: '',
87
87
  });
88
88
  });
@@ -1,3 +1,3 @@
1
1
  export default {
2
- openDatepickerLabel: 'Selector de fecha abierta',
2
+ openDatepickerLabel: 'Abrir selector de fecha',
3
3
  };
@@ -21,6 +21,19 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
21
21
  .input-group__container > .input-group__input ::slotted(.form-control) {
22
22
  text-align: center;
23
23
  }
24
+
25
+ .input-stepper__value {
26
+ position: absolute;
27
+ width: 1px;
28
+ height: 1px;
29
+ overflow: hidden;
30
+ clip-path: inset(100%);
31
+ clip: rect(1px, 1px, 1px, 1px);
32
+ white-space: nowrap;
33
+ border: 0;
34
+ margin: 0;
35
+ padding: 0;
36
+ }
24
37
  `,
25
38
  ];
26
39
  }
@@ -36,6 +49,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
36
49
  type: Number,
37
50
  reflect: true,
38
51
  },
52
+ valueTextMapping: {
53
+ type: Object,
54
+ },
39
55
  step: {
40
56
  type: Number,
41
57
  reflect: true,
@@ -66,6 +82,11 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
66
82
  this.formatter = formatNumber;
67
83
  this.min = Infinity;
68
84
  this.max = Infinity;
85
+ /**
86
+ * The aria-valuetext attribute defines the human-readable text alternative of aria-valuenow.
87
+ * @type {{[key: number]: string}}
88
+ */
89
+ this.valueTextMapping = {};
69
90
  this.step = 1;
70
91
  this.values = {
71
92
  max: this.max,
@@ -75,7 +96,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
75
96
 
76
97
  this.__increment = this.__increment.bind(this);
77
98
  this.__decrement = this.__decrement.bind(this);
78
- this.__boundOnLeaveButton = this._onLeaveButton.bind(this);
99
+ this._onEnterButton = this._onEnterButton.bind(this);
100
+ this._onLeaveButton = this._onLeaveButton.bind(this);
79
101
  }
80
102
 
81
103
  connectedCallback() {
@@ -85,11 +107,12 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
85
107
  min: this.min,
86
108
  step: this.step,
87
109
  };
88
-
89
- this.role = 'spinbutton';
110
+ if (this._inputNode) {
111
+ this._inputNode.role = 'spinbutton';
112
+ this._inputNode.setAttribute('inputmode', 'decimal');
113
+ this._inputNode.setAttribute('autocomplete', 'off');
114
+ }
90
115
  this.addEventListener('keydown', this.__keyDownHandler);
91
- this._inputNode.setAttribute('inputmode', 'decimal');
92
- this._inputNode.setAttribute('autocomplete', 'off');
93
116
  this.__setDefaultValidators();
94
117
  this.__toggleSpinnerButtonsState();
95
118
  }
@@ -110,27 +133,33 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
110
133
  if (changedProperties.has('min')) {
111
134
  this._inputNode.min = `${this.min}`;
112
135
  this.values.min = this.min;
136
+ if (this.min !== Infinity) {
137
+ this._inputNode.setAttribute('aria-valuemin', `${this.min}`);
138
+ } else {
139
+ this._inputNode.removeAttribute('aria-valuemin');
140
+ }
113
141
  this.__toggleSpinnerButtonsState();
114
142
  }
115
143
 
116
144
  if (changedProperties.has('max')) {
117
145
  this._inputNode.max = `${this.max}`;
118
146
  this.values.max = this.max;
147
+ if (this.max !== Infinity) {
148
+ this._inputNode.setAttribute('aria-valuemax', `${this.max}`);
149
+ } else {
150
+ this._inputNode.removeAttribute('aria-valuemax');
151
+ }
119
152
  this.__toggleSpinnerButtonsState();
120
153
  }
121
154
 
155
+ if (changedProperties.has('valueTextMapping')) {
156
+ this._updateAriaAttributes();
157
+ }
158
+
122
159
  if (changedProperties.has('step')) {
123
160
  this._inputNode.step = `${this.step}`;
124
161
  this.values.step = this.step;
125
162
  }
126
-
127
- if (changedProperties.has('_ariaLabelledNodes')) {
128
- this.__reflectAriaAttrToSpinButton('aria-labelledby', this._ariaLabelledNodes);
129
- }
130
-
131
- if (changedProperties.has('_ariaDescribedNodes')) {
132
- this.__reflectAriaAttrToSpinButton('aria-describedby', this._ariaDescribedNodes);
133
- }
134
163
  }
135
164
 
136
165
  get slots() {
@@ -141,22 +170,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
141
170
  };
142
171
  }
143
172
 
144
- /**
145
- * Based on FormControlMixin __reflectAriaAttr()
146
- *
147
- * Will handle help text, validation feedback and character counter,
148
- * prefix/suffix/before/after (if they contain data-description flag attr).
149
- * Also, contents of id references that will be put in the <lion-field>._ariaDescribedby property
150
- * from an external context, will be read by a screen reader.
151
- * @param {string} attrName
152
- * @param {Element[]} nodes
153
- * @private
154
- */
155
- __reflectAriaAttrToSpinButton(attrName, nodes) {
156
- const string = nodes.map(n => n.id).join(' ');
157
- this.setAttribute(attrName, string);
158
- }
159
-
160
173
  /**
161
174
  * Set aria labels and apply validators
162
175
  * @private
@@ -193,8 +206,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
193
206
  */
194
207
  __toggleSpinnerButtonsState() {
195
208
  const { min, max } = this.values;
196
- const decrementButton = this.__getSlot('prefix');
197
- const incrementButton = this.__getSlot('suffix');
209
+ const decrementButton = /** @type {HTMLButtonElement} */ (this.__getSlot('prefix'));
210
+ const incrementButton = /** @type {HTMLButtonElement} */ (this.__getSlot('suffix'));
198
211
  const disableIncrementor = this.currentValue >= max && max !== Infinity;
199
212
  const disableDecrementor = this.currentValue <= min && min !== Infinity;
200
213
  if (
@@ -205,7 +218,32 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
205
218
  }
206
219
  decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
207
220
  incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
208
- this.setAttribute('aria-valuenow', `${this.currentValue}`);
221
+ this._updateAriaAttributes();
222
+ }
223
+
224
+ /**
225
+ * @protected
226
+ */
227
+ _updateAriaAttributes() {
228
+ const displayValue = this._inputNode.value;
229
+ if (displayValue) {
230
+ this._inputNode.setAttribute('aria-valuenow', `${displayValue}`);
231
+ if (
232
+ Object.keys(this.valueTextMapping).length !== 0 &&
233
+ Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue)
234
+ ) {
235
+ this.__valueText = this.valueTextMapping[this.currentValue];
236
+ } else {
237
+ // VoiceOver announces percentages once the valuemin or valuemax are used.
238
+ // This can be fixed by setting valuetext to the same value as valuenow
239
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
240
+ this.__valueText = displayValue;
241
+ }
242
+ this._inputNode.setAttribute('aria-valuetext', `${this.__valueText}`);
243
+ } else {
244
+ this._inputNode.removeAttribute('aria-valuenow');
245
+ this._inputNode.removeAttribute('aria-valuetext');
246
+ }
209
247
  }
210
248
 
211
249
  /**
@@ -326,7 +364,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
326
364
  <button
327
365
  ?disabled=${this.disabled || this.readOnly}
328
366
  @click=${this.__decrement}
329
- @blur=${this.__boundOnLeaveButton}
367
+ @focus=${this._onEnterButton}
368
+ @blur=${this._onLeaveButton}
330
369
  type="button"
331
370
  aria-label="${this.msgLit('lion-input-stepper:decrease')}"
332
371
  >
@@ -345,7 +384,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
345
384
  <button
346
385
  ?disabled=${this.disabled || this.readOnly}
347
386
  @click=${this.__increment}
348
- @blur=${this.__boundOnLeaveButton}
387
+ @focus=${this._onEnterButton}
388
+ @blur=${this._onLeaveButton}
349
389
  type="button"
350
390
  aria-label="${this.msgLit('lion-input-stepper:increase')}"
351
391
  >
@@ -354,6 +394,33 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
354
394
  `;
355
395
  }
356
396
 
397
+ /** @protected */
398
+ _inputGroupTemplate() {
399
+ return html`
400
+ <div class="input-stepper__value">${this.__valueText}</div>
401
+ <div class="input-group">
402
+ ${this._inputGroupBeforeTemplate()}
403
+ <div class="input-group__container">
404
+ ${this._inputGroupPrefixTemplate()} ${this._inputGroupInputTemplate()}
405
+ ${this._inputGroupSuffixTemplate()}
406
+ </div>
407
+ ${this._inputGroupAfterTemplate()}
408
+ </div>
409
+ `;
410
+ }
411
+
412
+ /**
413
+ * @protected
414
+ * @param {Event} ev
415
+ */
416
+ // eslint-disable-next-line no-unused-vars
417
+ _onEnterButton(ev) {
418
+ const valueNode = /** @type {HTMLElement} */ (
419
+ this.shadowRoot?.querySelector('.input-stepper__value')
420
+ );
421
+ valueNode.setAttribute('aria-live', 'assertive');
422
+ }
423
+
357
424
  /**
358
425
  * Redispatch leave event on host when catching leave event
359
426
  * on the incrementor and decrementor button.
@@ -363,8 +430,16 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
363
430
  *
364
431
  * Interacting with the buttons is "user interactions"
365
432
  * the same way as focusing + blurring the field (native input)
433
+ *
434
+ * @protected
435
+ * @param {Event} ev
366
436
  */
367
- _onLeaveButton() {
437
+ // eslint-disable-next-line no-unused-vars
438
+ _onLeaveButton(ev) {
439
+ const valueNode = /** @type {HTMLElement} */ (
440
+ this.shadowRoot?.querySelector('.input-stepper__value')
441
+ );
442
+ valueNode.removeAttribute('aria-live');
368
443
  this.dispatchEvent(new Event(this._leaveEvent));
369
444
  }
370
445
  }
@@ -278,11 +278,106 @@ describe('<lion-input-stepper>', () => {
278
278
  await expect(el).to.be.accessible();
279
279
  });
280
280
 
281
+ it('has role="spinbutton"', async () => {
282
+ const el = await fixture(defaultInputStepper);
283
+ expect(el._inputNode.hasAttribute('role')).to.be.true;
284
+ expect(el._inputNode.getAttribute('role')).to.equal('spinbutton');
285
+ });
286
+
281
287
  it('updates aria-valuenow when stepper is changed', async () => {
282
288
  const el = await fixture(defaultInputStepper);
289
+ el.modelValue = 1;
290
+
291
+ await el.updateComplete;
292
+ expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.true;
293
+ expect(el._inputNode.getAttribute('aria-valuenow')).to.equal('1');
294
+
295
+ el.modelValue = '';
296
+ await el.updateComplete;
297
+ expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.false;
298
+ });
299
+
300
+ it('updates aria-valuetext when stepper is changed', async () => {
301
+ // VoiceOver announces percentages once the valuemin or valuemax are used.
302
+ // This can be fixed by setting valuetext to the same value as valuenow
303
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
304
+ const el = await fixture(defaultInputStepper);
305
+ el.modelValue = 1;
306
+ await el.updateComplete;
307
+
308
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
309
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('1');
310
+
311
+ el.modelValue = '';
312
+ await el.updateComplete;
313
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.false;
314
+ });
315
+
316
+ it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
317
+ const values = {
318
+ 1: 'first',
319
+ 2: 'second',
320
+ 3: 'third',
321
+ };
322
+ const el = await fixture(html`
323
+ <lion-input-stepper min="1" max="3" .valueTextMapping="${values}"></lion-input-stepper>
324
+ `);
325
+ el.modelValue = 1;
326
+ await el.updateComplete;
327
+ expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
328
+ expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('first');
329
+ });
330
+
331
+ it('updates aria-valuemin when stepper is changed', async () => {
332
+ const el = await fixture(inputStepperWithAttrs);
283
333
  const incrementButton = el.querySelector('[slot=suffix]');
284
334
  incrementButton?.dispatchEvent(new Event('click'));
285
- expect(el).to.have.attribute('aria-valuenow', '1');
335
+ expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
336
+ expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('100');
337
+
338
+ el.min = 0;
339
+ await el.updateComplete;
340
+ expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
341
+ expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('0');
342
+ });
343
+
344
+ it('updates aria-valuemax when stepper is changed', async () => {
345
+ const el = await fixture(inputStepperWithAttrs);
346
+ const incrementButton = el.querySelector('[slot=suffix]');
347
+ incrementButton?.dispatchEvent(new Event('click'));
348
+ expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
349
+ expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('200');
350
+
351
+ el.max = 1000;
352
+ await el.updateComplete;
353
+ expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
354
+ expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
355
+ });
356
+
357
+ it('when decrease button gets focus, it sets aria-live to input-stepper__value', async () => {
358
+ const el = await fixture(inputStepperWithAttrs);
359
+ const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
360
+ const decrementButton = el.querySelector('[slot=prefix]');
361
+
362
+ decrementButton?.dispatchEvent(new Event('focus'));
363
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
364
+ expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
365
+
366
+ decrementButton?.dispatchEvent(new Event('blur'));
367
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
368
+ });
369
+
370
+ it('when increase button gets focus, it sets aria-live to input-stepper__value', async () => {
371
+ const el = await fixture(inputStepperWithAttrs);
372
+ const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
373
+ const incrementButton = el.querySelector('[slot=suffix]');
374
+
375
+ incrementButton?.dispatchEvent(new Event('focus'));
376
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
377
+ expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
378
+
379
+ incrementButton?.dispatchEvent(new Event('blur'));
380
+ expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
286
381
  });
287
382
  });
288
383
  });
@@ -158,11 +158,11 @@ export class LionInputTelDropdown extends LionInputTel {
158
158
  super.styles,
159
159
  css`
160
160
  /**
161
- * We need to align the height of the dropdown with the height of the text field.
162
- * We target the HTMLDivElement (render wrapper from SlotMixin) here. Its child,
163
- * [data-ref=dropdown], recieves a 100% height as well via inline styles (since we
164
- * can't target from shadow styles).
165
- */
161
+ * We need to align the height of the dropdown with the height of the text field.
162
+ * We target the HTMLDivElement (render wrapper from SlotMixin) here. Its child,
163
+ * [data-ref=dropdown], receives a 100% height as well via inline styles (since we
164
+ * can't target from shadow styles).
165
+ */
166
166
  ::slotted([slot='prefix']) {
167
167
  height: 100%;
168
168
  }
@@ -301,7 +301,8 @@ export class LionInputTelDropdown extends LionInputTel {
301
301
  } else {
302
302
  this.__initializedRegionCode = `+${countryCode}`;
303
303
  }
304
- this.modelValue = this.__initializedRegionCode;
304
+ // on init we set the value to the _inputNode and not the modelValue to prevent validation
305
+ this._inputNode.value = this.__initializedRegionCode;
305
306
  this._initialModelValue = this.__initializedRegionCode;
306
307
  this.initInteractionState();
307
308
  }