@lion/ui 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/components/core/src/DisabledMixin.js +6 -0
  2. package/components/core/test/DisabledMixin.test.js +22 -1
  3. package/components/core/test/SlotMixin.test.js +3 -3
  4. package/components/form-core/test/FocusMixin.test.js +6 -4
  5. package/components/form-core/test-suites/NativeTextFieldMixin.suite.js +4 -8
  6. package/components/input-stepper/src/LionInputStepper.js +50 -3
  7. package/components/input-stepper/test/lion-input-stepper.test.js +60 -1
  8. package/components/input-tel-dropdown/test/LionInputTelDropdown.test.js +12 -10
  9. package/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.js +16 -12
  10. package/components/overlays/src/OverlayController.js +9 -0
  11. package/components/overlays/test/OverlayController.test.js +27 -3
  12. package/components/pagination/src/LionPagination.js +1 -0
  13. package/components/pagination/test/lion-pagination.test.js +16 -0
  14. package/components/select-rich/test/lion-select-rich.test.js +9 -8
  15. package/components/switch/test/lion-switch.test.js +1 -1
  16. package/components/tabs/test/lion-tabs.test.js +12 -5
  17. package/custom-elements.json +471 -30
  18. package/dist-types/components/core/src/DisabledMixin.d.ts.map +1 -1
  19. package/dist-types/components/form-core/test-suites/NativeTextFieldMixin.suite.d.ts.map +1 -1
  20. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +11 -0
  21. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  22. package/dist-types/components/input-tel-dropdown/test/LionInputTelDropdown.test.d.ts +2 -2
  23. package/dist-types/components/input-tel-dropdown/test/LionInputTelDropdown.test.d.ts.map +1 -1
  24. package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.d.ts +2 -2
  25. package/dist-types/components/input-tel-dropdown/test-suites/LionInputTelDropdown.suite.d.ts.map +1 -1
  26. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  27. package/dist-types/components/pagination/src/LionPagination.d.ts.map +1 -1
  28. package/dist-types/components/tabs/test/lion-tabs.test.d.ts.map +1 -1
  29. package/dist-types/tsconfig.tsbuildinfo +1 -1
  30. package/docs/components/button/api-table.md +3 -0
  31. package/docs/components/checkbox-group/api-table.md +3 -0
  32. package/docs/components/checkbox-group/overview.md +2 -2
  33. package/docs/components/collapsible/overview.md +1 -1
  34. package/docs/components/combobox/api-table.md +1 -0
  35. package/docs/components/combobox/overview.md +1 -1
  36. package/docs/components/combobox/use-cases.md +1 -1
  37. package/docs/components/dialog/overview.md +1 -1
  38. package/docs/components/fieldset/api-table.md +1 -0
  39. package/docs/components/fieldset/overview.md +5 -5
  40. package/docs/components/form/overview.md +3 -3
  41. package/docs/components/form/use-cases.md +2 -2
  42. package/docs/components/icon/use-cases.md +1 -1
  43. package/docs/components/input/api-table.md +1 -0
  44. package/docs/components/input/overview.md +1 -1
  45. package/docs/components/input-amount/api-table.md +1 -0
  46. package/docs/components/input-amount/overview.md +3 -3
  47. package/docs/components/input-date/api-table.md +1 -0
  48. package/docs/components/input-date/overview.md +3 -3
  49. package/docs/components/input-datepicker/api-table.md +1 -0
  50. package/docs/components/input-datepicker/overview.md +5 -5
  51. package/docs/components/input-email/api-table.md +1 -0
  52. package/docs/components/input-email/overview.md +2 -2
  53. package/docs/components/input-file/api-table.md +1 -0
  54. package/docs/components/input-file/overview.md +1 -1
  55. package/docs/components/input-iban/api-table.md +1 -0
  56. package/docs/components/input-iban/overview.md +2 -2
  57. package/docs/components/input-range/api-table.md +1 -0
  58. package/docs/components/input-range/overview.md +2 -2
  59. package/docs/components/input-stepper/api-table.md +4 -0
  60. package/docs/components/input-stepper/overview.md +1 -1
  61. package/docs/components/input-stepper/use-cases.md +30 -0
  62. package/docs/components/input-tel/api-table.md +1 -0
  63. package/docs/components/input-tel/overview.md +1 -1
  64. package/docs/components/input-tel/use-cases.md +6 -6
  65. package/docs/components/input-tel-dropdown/api-table.md +1 -0
  66. package/docs/components/input-tel-dropdown/overview.md +1 -1
  67. package/docs/components/listbox/api-table.md +2 -0
  68. package/docs/components/listbox/use-cases.md +1 -1
  69. package/docs/components/radio-group/api-table.md +2 -0
  70. package/docs/components/radio-group/overview.md +3 -3
  71. package/docs/components/select/api-table.md +1 -0
  72. package/docs/components/select/use-cases.md +1 -1
  73. package/docs/components/select-rich/api-table.md +2 -0
  74. package/docs/components/select-rich/use-cases.md +1 -1
  75. package/docs/components/switch/api-table.md +2 -0
  76. package/docs/components/tabs/examples.md +1 -1
  77. package/docs/components/textarea/api-table.md +1 -0
  78. package/docs/fundamentals/index.md +4 -4
  79. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  80. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  81. package/docs/fundamentals/rationales/accessibility.md +1 -1
  82. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  83. package/docs/fundamentals/systems/core/overview.md +1 -1
  84. package/docs/fundamentals/systems/form/api-table.md +1 -0
  85. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  86. package/docs/fundamentals/systems/form/model-value.md +1 -1
  87. package/docs/fundamentals/systems/form/overview.md +28 -28
  88. package/docs/fundamentals/systems/form/validate.md +2 -2
  89. package/docs/fundamentals/systems/localize/overview.md +4 -4
  90. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  91. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  92. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  93. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  94. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  95. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  96. package/docs/guides/how-to/create-a-form.md +4 -4
  97. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  98. package/docs/guides/index.md +8 -8
  99. package/docs/guides/principles/index.md +5 -5
  100. package/docs/guides/principles/styling.md +1 -1
  101. package/package.json +1 -1
@@ -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
  }
@@ -36,6 +36,9 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
36
36
  type: Number,
37
37
  reflect: true,
38
38
  },
39
+ valueTextMapping: {
40
+ type: Object,
41
+ },
39
42
  step: {
40
43
  type: Number,
41
44
  reflect: true,
@@ -66,6 +69,11 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
66
69
  this.formatter = formatNumber;
67
70
  this.min = Infinity;
68
71
  this.max = Infinity;
72
+ /**
73
+ * The aria-valuetext attribute defines the human-readable text alternative of aria-valuenow.
74
+ * @type {{[key: number]: string}}
75
+ */
76
+ this.valueTextMapping = {};
69
77
  this.step = 1;
70
78
  this.values = {
71
79
  max: this.max,
@@ -110,15 +118,29 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
110
118
  if (changedProperties.has('min')) {
111
119
  this._inputNode.min = `${this.min}`;
112
120
  this.values.min = this.min;
121
+ if (this.min !== Infinity) {
122
+ this.setAttribute('aria-valuemin', `${this.min}`);
123
+ } else {
124
+ this.removeAttribute('aria-valuemin');
125
+ }
113
126
  this.__toggleSpinnerButtonsState();
114
127
  }
115
128
 
116
129
  if (changedProperties.has('max')) {
117
130
  this._inputNode.max = `${this.max}`;
118
131
  this.values.max = this.max;
132
+ if (this.max !== Infinity) {
133
+ this.setAttribute('aria-valuemax', `${this.max}`);
134
+ } else {
135
+ this.removeAttribute('aria-valuemax');
136
+ }
119
137
  this.__toggleSpinnerButtonsState();
120
138
  }
121
139
 
140
+ if (changedProperties.has('valueTextMapping')) {
141
+ this._updateAriaAttributes();
142
+ }
143
+
122
144
  if (changedProperties.has('step')) {
123
145
  this._inputNode.step = `${this.step}`;
124
146
  this.values.step = this.step;
@@ -193,8 +215,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
193
215
  */
194
216
  __toggleSpinnerButtonsState() {
195
217
  const { min, max } = this.values;
196
- const decrementButton = this.__getSlot('prefix');
197
- const incrementButton = this.__getSlot('suffix');
218
+ const decrementButton = /** @type {HTMLButtonElement} */ (this.__getSlot('prefix'));
219
+ const incrementButton = /** @type {HTMLButtonElement} */ (this.__getSlot('suffix'));
198
220
  const disableIncrementor = this.currentValue >= max && max !== Infinity;
199
221
  const disableDecrementor = this.currentValue <= min && min !== Infinity;
200
222
  if (
@@ -205,7 +227,32 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
205
227
  }
206
228
  decrementButton[disableDecrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
207
229
  incrementButton[disableIncrementor ? 'setAttribute' : 'removeAttribute']('disabled', 'true');
208
- this.setAttribute('aria-valuenow', `${this.currentValue}`);
230
+
231
+ this._updateAriaAttributes();
232
+ }
233
+
234
+ /**
235
+ * @protected
236
+ */
237
+ _updateAriaAttributes() {
238
+ const displayValue = this._inputNode.value;
239
+ if (displayValue) {
240
+ this.setAttribute('aria-valuenow', `${displayValue}`);
241
+ if (
242
+ Object.keys(this.valueTextMapping).length !== 0 &&
243
+ Object.keys(this.valueTextMapping).find(key => Number(key) === this.currentValue)
244
+ ) {
245
+ this.setAttribute('aria-valuetext', `${this.valueTextMapping[this.currentValue]}`);
246
+ } else {
247
+ // VoiceOver announces percentages once the valuemin or valuemax are used.
248
+ // This can be fixed by setting valuetext to the same value as valuenow
249
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
250
+ this.setAttribute('aria-valuetext', `${displayValue}`);
251
+ }
252
+ } else {
253
+ this.removeAttribute('aria-valuenow');
254
+ this.removeAttribute('aria-valuetext');
255
+ }
209
256
  }
210
257
 
211
258
  /**
@@ -280,9 +280,68 @@ describe('<lion-input-stepper>', () => {
280
280
 
281
281
  it('updates aria-valuenow when stepper is changed', async () => {
282
282
  const el = await fixture(defaultInputStepper);
283
+ el.modelValue = 1;
284
+
285
+ await el.updateComplete;
286
+ expect(el.hasAttribute('aria-valuenow')).to.be.true;
287
+ expect(el.getAttribute('aria-valuenow')).to.equal('1');
288
+
289
+ el.modelValue = '';
290
+ await el.updateComplete;
291
+ expect(el.hasAttribute('aria-valuenow')).to.be.false;
292
+ });
293
+
294
+ it('updates aria-valuetext when stepper is changed', async () => {
295
+ // VoiceOver announces percentages once the valuemin or valuemax are used.
296
+ // This can be fixed by setting valuetext to the same value as valuenow
297
+ // https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
298
+ const el = await fixture(defaultInputStepper);
299
+ el.modelValue = 1;
300
+ await el.updateComplete;
301
+
302
+ expect(el.hasAttribute('aria-valuetext')).to.be.true;
303
+ expect(el.getAttribute('aria-valuetext')).to.equal('1');
304
+
305
+ el.modelValue = '';
306
+ await el.updateComplete;
307
+ expect(el.hasAttribute('aria-valuetext')).to.be.false;
308
+ });
309
+
310
+ it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
311
+ const values = {
312
+ 1: 'first',
313
+ 2: 'second',
314
+ 3: 'third',
315
+ };
316
+ const el = await fixture(html`
317
+ <lion-input-stepper min="1" max="3" .valueTextMapping="${values}"></lion-input-stepper>
318
+ `);
319
+ el.modelValue = 1;
320
+ await el.updateComplete;
321
+ expect(el.hasAttribute('aria-valuetext')).to.be.true;
322
+ expect(el.getAttribute('aria-valuetext')).to.equal('first');
323
+ });
324
+
325
+ it('updates aria-valuemin when stepper is changed', async () => {
326
+ const el = await fixture(inputStepperWithAttrs);
327
+ const incrementButton = el.querySelector('[slot=suffix]');
328
+ incrementButton?.dispatchEvent(new Event('click'));
329
+ expect(el).to.have.attribute('aria-valuemin', '100');
330
+
331
+ el.min = 0;
332
+ await el.updateComplete;
333
+ expect(el).to.have.attribute('aria-valuemin', '0');
334
+ });
335
+
336
+ it('updates aria-valuemax when stepper is changed', async () => {
337
+ const el = await fixture(inputStepperWithAttrs);
283
338
  const incrementButton = el.querySelector('[slot=suffix]');
284
339
  incrementButton?.dispatchEvent(new Event('click'));
285
- expect(el).to.have.attribute('aria-valuenow', '1');
340
+ expect(el).to.have.attribute('aria-valuemax', '200');
341
+
342
+ el.max = 1000;
343
+ await el.updateComplete;
344
+ expect(el).to.have.attribute('aria-valuemax', '1000');
286
345
  });
287
346
  });
288
347
  });
@@ -1,20 +1,22 @@
1
+ import { mimicUserChangingDropdown } from '@lion/ui/input-tel-dropdown-test-helpers.js';
2
+ import { runInputTelDropdownSuite } from '@lion/ui/input-tel-dropdown-test-suites.js';
3
+ import { LionInputTelDropdown } from '@lion/ui/input-tel-dropdown.js';
1
4
  import { runInputTelSuite } from '@lion/ui/input-tel-test-suites.js';
5
+ import { aTimeout, expect, fixture } from '@open-wc/testing';
6
+ import { LionSelectRich } from '@lion/ui/select-rich.js';
2
7
  import { repeat } from 'lit/directives/repeat.js';
8
+ import { LionOption } from '@lion/ui/listbox.js';
3
9
  import { ref } from 'lit/directives/ref.js';
4
10
  import { html } from 'lit';
5
- import { aTimeout, expect, fixture } from '@open-wc/testing';
6
- import { LionInputTelDropdown } from '@lion/ui/input-tel-dropdown.js';
7
- import { LionOption } from '@lion/ui/listbox.js';
8
- import { LionSelectRich } from '@lion/ui/select-rich.js';
9
- import { runInputTelDropdownSuite } from '@lion/ui/input-tel-dropdown-test-suites.js';
10
- import { mimicUserChangingDropdown } from '@lion/ui/input-tel-dropdown-test-helpers.js';
11
+
12
+ import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
11
13
  import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
12
14
 
13
15
  /**
14
- * @typedef {import('lit').TemplateResult} TemplateResult
15
- * @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement
16
16
  * @typedef {import('../types/index.js').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
17
+ * @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement
17
18
  * @typedef {import('../types/index.js').RegionMeta} RegionMeta
19
+ * @typedef {import('lit').TemplateResult} TemplateResult
18
20
  */
19
21
 
20
22
  class WithFormControlInputTelDropdown extends ScopedElementsMixin(LionInputTelDropdown) {
@@ -114,7 +116,7 @@ describe('WithFormControlInputTelDropdown', () => {
114
116
  await el.updateComplete;
115
117
  await aTimeout(0);
116
118
  // @ts-expect-error [allow-protected-in-tests]
117
- expect(el._inputNode).to.equal(document.activeElement);
119
+ expect(isActiveElement(el._inputNode)).to.be.true;
118
120
  }
119
121
  });
120
122
 
@@ -167,7 +169,7 @@ describe('WithFormControlInputTelDropdown', () => {
167
169
  await el.updateComplete;
168
170
  await aTimeout(0);
169
171
  // @ts-expect-error [allow-protected-in-tests]
170
- expect(el._inputNode).to.not.equal(document.activeElement);
172
+ expect(isActiveElement(el._inputNode)).to.be.false;
171
173
  }
172
174
  });
173
175
  });
@@ -1,22 +1,24 @@
1
- import { PhoneUtilManager } from '@lion/ui/input-tel.js';
2
1
  import { mockPhoneUtilManager, restorePhoneUtilManager } from '@lion/ui/input-tel-test-helpers.js';
2
+ import { mimicUserChangingDropdown } from '@lion/ui/input-tel-dropdown-test-helpers.js';
3
+ import { LionInputTelDropdown } from '@lion/ui/input-tel-dropdown.js';
4
+ import { PhoneUtilManager } from '@lion/ui/input-tel.js';
5
+ import sinon from 'sinon';
3
6
  import {
7
+ fixtureSync as _fixtureSync,
8
+ fixture as _fixture,
9
+ unsafeStatic,
4
10
  aTimeout,
5
11
  defineCE,
6
12
  expect,
7
- fixture as _fixture,
8
- fixtureSync as _fixtureSync,
9
13
  html,
10
- unsafeStatic,
11
14
  } from '@open-wc/testing';
12
- import sinon from 'sinon';
13
- import { LionInputTelDropdown } from '@lion/ui/input-tel-dropdown.js';
14
- import { mimicUserChangingDropdown } from '@lion/ui/input-tel-dropdown-test-helpers.js';
15
+
16
+ import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
15
17
 
16
18
  /**
17
- * @typedef {import('lit').TemplateResult} TemplateResult
18
- * @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement
19
19
  * @typedef {import('../types/index.js').TemplateDataForDropdownInputTel} TemplateDataForDropdownInputTel
20
+ * @typedef {HTMLSelectElement|HTMLElement & {modelValue:string}} DropdownElement
21
+ * @typedef {import('lit').TemplateResult} TemplateResult
20
22
  */
21
23
 
22
24
  const fixture = /** @type {(arg: string | TemplateResult) => Promise<LionInputTelDropdown>} */ (
@@ -206,12 +208,14 @@ export function runInputTelDropdownSuite({ klass } = { klass: LionInputTelDropdo
206
208
 
207
209
  it('syncs disabled attribute to dropdown', async () => {
208
210
  const el = await fixture(html` <${tag} disabled></${tag}> `);
209
- expect(el.refs.dropdown.value?.hasAttribute('disabled')).to.be.true;
211
+ expect(/** @type {HTMLElement} */ (el.refs.dropdown.value)?.hasAttribute('disabled')).to.be
212
+ .true;
210
213
  });
211
214
 
212
215
  it('disables dropdown on readonly', async () => {
213
216
  const el = await fixture(html` <${tag} readonly></${tag}> `);
214
- expect(el.refs.dropdown.value?.hasAttribute('disabled')).to.be.true;
217
+ expect(/** @type {HTMLElement} */ (el.refs.dropdown.value)?.hasAttribute('disabled')).to.be
218
+ .true;
215
219
  });
216
220
 
217
221
  it('renders to prefix slot in light dom', async () => {
@@ -332,7 +336,7 @@ export function runInputTelDropdownSuite({ klass } = { klass: LionInputTelDropdo
332
336
  mimicUserChangingDropdown(dropdownElement, 'BE');
333
337
  await el.updateComplete;
334
338
  // @ts-expect-error [allow-protected-in-tests]
335
- expect(el._inputNode).to.not.equal(document.activeElement);
339
+ expect(isActiveElement(el._inputNode)).to.be.false;
336
340
  });
337
341
  });
338
342
 
@@ -1121,6 +1121,15 @@ export class OverlayController extends EventTarget {
1121
1121
  if (this.manager) {
1122
1122
  this.manager.disableTrapsKeyboardFocusForAll();
1123
1123
  }
1124
+
1125
+ const isContentShadowHost = Boolean(this.contentNode.shadowRoot);
1126
+ if (isContentShadowHost) {
1127
+ // eslint-disable-next-line no-console
1128
+ console.warn(
1129
+ '[overlays]: For best accessibility (compatibility with Safari + VoiceOver), provide a contentNode that is not a host for a shadow root',
1130
+ );
1131
+ }
1132
+
1124
1133
  this._containFocusHandler = containFocus(this.contentNode);
1125
1134
  this.__hasActiveTrapsKeyboardFocus = true;
1126
1135
  if (this.manager) {
@@ -603,8 +603,8 @@ describe('OverlayController', () => {
603
603
  event.keyCode = keyCodes.tab;
604
604
  window.dispatchEvent(event);
605
605
 
606
- expect(elOutside).to.not.equal(document.activeElement);
607
- expect(input1).to.equal(document.activeElement);
606
+ expect(isActiveElement(elOutside)).to.be.false;
607
+ expect(isActiveElement(input1)).to.be.true;
608
608
  });
609
609
 
610
610
  it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => {
@@ -625,7 +625,7 @@ describe('OverlayController', () => {
625
625
  input.focus();
626
626
  simulateTab();
627
627
 
628
- expect(elOutside).to.equal(document.activeElement);
628
+ expect(isActiveElement(elOutside)).to.be.true;
629
629
  });
630
630
 
631
631
  it('keeps focus within overlay with multiple overlays with all traps on true', async () => {
@@ -648,6 +648,30 @@ describe('OverlayController', () => {
648
648
  expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.true;
649
649
  expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.false;
650
650
  });
651
+
652
+ it('warns when contentNode is a host for a shadowRoot', async () => {
653
+ const warnSpy = sinon.spy(console, 'warn');
654
+
655
+ const contentNode = /** @type {HTMLDivElement} */ (await fixture(html` <div></div> `));
656
+ contentNode.attachShadow({ mode: 'open' });
657
+ const shadowRootContentNode = /** @type {HTMLElement} */ (
658
+ await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
659
+ );
660
+ contentNode.shadowRoot?.appendChild(shadowRootContentNode);
661
+
662
+ const ctrl = new OverlayController({
663
+ ...withGlobalTestConfig(),
664
+ trapsKeyboardFocus: true,
665
+ contentNode,
666
+ });
667
+ await ctrl.show();
668
+
669
+ // @ts-expect-error
670
+ expect(console.warn.args[0][0]).to.equal(
671
+ '[overlays]: For best accessibility (compatibility with Safari + VoiceOver), provide a contentNode that is not a host for a shadow root',
672
+ );
673
+ warnSpy.restore();
674
+ });
651
675
  });
652
676
 
653
677
  describe('hidesOnEsc', () => {
@@ -287,6 +287,7 @@ export class LionPagination extends LocalizeMixin(LitElement) {
287
287
  <button
288
288
  aria-label="${this.msgLit('lion-pagination:page', { page })}"
289
289
  aria-current=${page === this.current}
290
+ aria-live="${page === this.current ? 'polite' : 'off'}"
290
291
  @click=${() => this.__fire(page)}
291
292
  >
292
293
  ${page}
@@ -105,6 +105,22 @@ describe('Pagination', () => {
105
105
  expect(el.current).to.equal(2);
106
106
  });
107
107
 
108
+ it('should announce next and previous page using `next()` and `previous()`', async () => {
109
+ const el = await fixture(html` <lion-pagination count="6" current="2"></lion-pagination> `);
110
+ const buttons = Array.from(
111
+ /** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('button'),
112
+ );
113
+ expect(buttons[2].getAttribute('aria-live')).to.equal('polite');
114
+ el.next();
115
+ await el.updateComplete;
116
+ expect(buttons[3].getAttribute('aria-live')).to.equal('polite');
117
+ expect(buttons[2].getAttribute('aria-live')).to.equal('off');
118
+ el.previous();
119
+ await el.updateComplete;
120
+ expect(buttons[3].getAttribute('aria-live')).to.equal('off');
121
+ expect(buttons[2].getAttribute('aria-live')).to.equal('polite');
122
+ });
123
+
108
124
  it('should goto next and previous page using `next()` and `previous()`', async () => {
109
125
  const el = await fixture(html` <lion-pagination count="6" current="2"></lion-pagination> `);
110
126
  el.next();
@@ -31,10 +31,11 @@ const fixture = /** @type {(arg: TemplateResult) => Promise<LionSelectRich>} */
31
31
  describe('lion-select-rich', () => {
32
32
  it('clicking the label should focus the invoker', async () => {
33
33
  const el = await fixture(html` <lion-select-rich label="foo"> </lion-select-rich> `);
34
- expect(document.activeElement === document.body).to.be.true;
34
+ expect(isActiveElement(document.body)).to.be.true;
35
+
35
36
  const { _labelNode, _invokerNode } = getSelectRichMembers(el);
36
37
  _labelNode.click();
37
- expect(document.activeElement === _invokerNode).to.be.true;
38
+ expect(isActiveElement(_invokerNode)).to.be.true;
38
39
  });
39
40
 
40
41
  it('has an attribute focused when focused', async () => {
@@ -276,14 +277,14 @@ describe('lion-select-rich', () => {
276
277
 
277
278
  await _overlayCtrl.show();
278
279
  await el.updateComplete;
279
- expect(document.activeElement === _listboxNode).to.be.true;
280
- expect(document.activeElement === _invokerNode).to.be.false;
280
+ expect(isActiveElement(_listboxNode)).to.be.true;
281
+ expect(isActiveElement(_invokerNode)).to.be.false;
281
282
 
282
283
  el.opened = false;
283
284
  await el.updateComplete;
284
285
  await el.updateComplete; // safari takes a little longer
285
- expect(document.activeElement === _listboxNode).to.be.false;
286
- expect(document.activeElement === _invokerNode).to.be.true;
286
+ expect(isActiveElement(_listboxNode)).to.be.false;
287
+ expect(isActiveElement(_invokerNode)).to.be.true;
287
288
  });
288
289
 
289
290
  it('opens the listbox with checked option as active', async () => {
@@ -472,7 +473,7 @@ describe('lion-select-rich', () => {
472
473
  <lion-select-rich .config=${{ trapsKeyboardFocus: true }}></lion-select-rich>
473
474
  `);
474
475
  const { _listboxNode } = getSelectRichMembers(el);
475
- expect(document.activeElement).to.not.equal(_listboxNode);
476
+ expect(isActiveElement(_listboxNode)).to.be.false;
476
477
 
477
478
  el.opened = true;
478
479
  await el.updateComplete;
@@ -481,7 +482,7 @@ describe('lion-select-rich', () => {
481
482
  el.opened = false;
482
483
  await el.updateComplete;
483
484
  await el.updateComplete; // safari takes a little longer
484
- expect(document.activeElement).to.not.equal(_listboxNode);
485
+ expect(isActiveElement(_listboxNode)).to.be.false;
485
486
  });
486
487
  });
487
488
 
@@ -69,7 +69,7 @@ describe('lion-switch', () => {
69
69
  const { _inputNode, _labelNode } = getSwitchMembers(el);
70
70
 
71
71
  _labelNode.click();
72
- expect(document.activeElement).to.not.equal(_inputNode);
72
+ expect(isActiveElement(_inputNode)).to.be.false;
73
73
  });
74
74
 
75
75
  it('should sync its "disabled" state to child button', async () => {
@@ -2,6 +2,8 @@ import { expect, fixture } from '@open-wc/testing';
2
2
  import { html } from 'lit/static-html.js';
3
3
  import sinon from 'sinon';
4
4
 
5
+ import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
6
+
5
7
  /**
6
8
  * @typedef {import('../src/LionTabs.js').LionTabs} LionTabs
7
9
  */
@@ -397,9 +399,11 @@ describe('<lion-tabs>', () => {
397
399
  `)
398
400
  );
399
401
 
402
+ const secondTab = /** @type {HTMLElement} */ (
403
+ el.querySelector('[slot="tab"]:nth-of-type(2)')
404
+ );
400
405
  el.selectedIndex = 1;
401
- expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be
402
- .false;
406
+ expect(isActiveElement(secondTab)).to.be.false;
403
407
  });
404
408
 
405
409
  it('does not focus a tab on firstUpdate', async () => {
@@ -414,7 +418,7 @@ describe('<lion-tabs>', () => {
414
418
  `)
415
419
  );
416
420
  const tabs = Array.from(el.children).filter(child => child.slot === 'tab');
417
- expect(tabs.some(tab => tab === document.activeElement)).to.be.false;
421
+ expect(tabs.some(tab => isActiveElement(tab))).to.be.false;
418
422
  });
419
423
 
420
424
  it('focuses on a tab when setting with _setSelectedIndexWithFocus method', async () => {
@@ -429,9 +433,12 @@ describe('<lion-tabs>', () => {
429
433
  `)
430
434
  );
431
435
 
436
+ const secondTab = /** @type {HTMLElement} */ (
437
+ el.querySelector('[slot="tab"]:nth-of-type(2)')
438
+ );
432
439
  // @ts-ignore : this el is LionTabs
433
440
  el._setSelectedIndexWithFocus(1);
434
- expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be.true;
441
+ expect(isActiveElement(secondTab)).to.be.true;
435
442
  });
436
443
  });
437
444
 
@@ -448,7 +455,7 @@ describe('<lion-tabs>', () => {
448
455
  );
449
456
  const secondTab = /** @type {Element} */ (el.querySelector('[slot="tab"]:nth-of-type(2)'));
450
457
  secondTab.dispatchEvent(new MouseEvent('click'));
451
- expect(secondTab === document.activeElement).to.be.true;
458
+ expect(isActiveElement(secondTab)).to.be.true;
452
459
  });
453
460
 
454
461
  describe('Accessibility', () => {