@lion/ui 0.15.4 → 0.16.0-alpha.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 (83) hide show
  1. package/components/collapsible/src/LionCollapsible.js +0 -2
  2. package/components/collapsible/test/lion-collapsible.test.js +0 -9
  3. package/components/core/test-helpers/ssr-definitions/ScopedElementsHost.define.js +1 -0
  4. package/components/drawer/test/lion-drawer.test.js +0 -9
  5. package/components/form-core/src/choice-group/ChoiceGroupMixin.js +12 -2
  6. package/components/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js +48 -4
  7. package/components/input-tel/translations/es.js +1 -1
  8. package/components/overlays/src/OverlayController.js +99 -66
  9. package/components/overlays/src/OverlaysManager.js +0 -33
  10. package/components/overlays/test/OverlayController.test.js +55 -32
  11. package/custom-elements.json +105 -200
  12. package/dist-types/components/collapsible/src/LionCollapsible.d.ts.map +1 -1
  13. package/dist-types/components/core/test-helpers/ssr-definitions/ScopedElementsHost.define.d.ts +2 -0
  14. package/dist-types/components/core/test-helpers/ssr-definitions/ScopedElementsHost.define.d.ts.map +1 -0
  15. package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts.map +1 -1
  16. package/dist-types/components/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.d.ts.map +1 -1
  17. package/dist-types/components/overlays/src/OverlayController.d.ts +1 -11
  18. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  19. package/dist-types/components/overlays/src/OverlaysManager.d.ts +0 -12
  20. package/dist-types/components/overlays/src/OverlaysManager.d.ts.map +1 -1
  21. package/dist-types/components/overlays/test/OverlayController.test.d.ts.map +1 -1
  22. package/dist-types/exports/overlays.d.ts +0 -1
  23. package/dist-types/tsconfig.tsbuildinfo +1 -1
  24. package/docs/components/checkbox-group/overview.md +2 -2
  25. package/docs/components/collapsible/overview.md +1 -1
  26. package/docs/components/combobox/overview.md +1 -1
  27. package/docs/components/combobox/use-cases.md +1 -1
  28. package/docs/components/dialog/overview.md +1 -1
  29. package/docs/components/fieldset/overview.md +5 -5
  30. package/docs/components/form/overview.md +3 -3
  31. package/docs/components/form/use-cases.md +2 -2
  32. package/docs/components/icon/use-cases.md +1 -1
  33. package/docs/components/input/overview.md +1 -1
  34. package/docs/components/input-amount/overview.md +3 -3
  35. package/docs/components/input-date/overview.md +3 -3
  36. package/docs/components/input-datepicker/overview.md +5 -5
  37. package/docs/components/input-email/overview.md +2 -2
  38. package/docs/components/input-file/overview.md +1 -1
  39. package/docs/components/input-iban/overview.md +2 -2
  40. package/docs/components/input-range/overview.md +2 -2
  41. package/docs/components/input-stepper/overview.md +1 -1
  42. package/docs/components/input-tel/overview.md +1 -1
  43. package/docs/components/input-tel/use-cases.md +6 -6
  44. package/docs/components/input-tel-dropdown/overview.md +1 -1
  45. package/docs/components/listbox/use-cases.md +1 -1
  46. package/docs/components/radio-group/overview.md +3 -3
  47. package/docs/components/select/use-cases.md +1 -1
  48. package/docs/components/select-rich/use-cases.md +2 -2
  49. package/docs/components/switch/use-cases.md +3 -3
  50. package/docs/components/tabs/examples.md +1 -1
  51. package/docs/fundamentals/index.md +4 -4
  52. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  53. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  54. package/docs/fundamentals/rationales/TypeScript.md +46 -0
  55. package/docs/fundamentals/rationales/accessibility.md +1 -1
  56. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  57. package/docs/fundamentals/systems/core/overview.md +1 -1
  58. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  59. package/docs/fundamentals/systems/form/model-value.md +1 -1
  60. package/docs/fundamentals/systems/form/overview.md +28 -28
  61. package/docs/fundamentals/systems/form/validate.md +2 -2
  62. package/docs/fundamentals/systems/localize/overview.md +4 -4
  63. package/docs/fundamentals/systems/overlays/api-table.md +45 -51
  64. package/docs/fundamentals/systems/overlays/configuration.md +5 -2
  65. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  66. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  67. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  68. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  69. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  70. package/docs/guides/how-to/create-a-form.md +4 -4
  71. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  72. package/docs/guides/index.md +8 -8
  73. package/docs/guides/principles/index.md +5 -5
  74. package/docs/guides/principles/styling.md +1 -1
  75. package/exports/overlays.js +0 -1
  76. package/overlays.js +0 -1
  77. package/package.json +1 -1
  78. package/components/overlays/src/utils/contain-focus.js +0 -202
  79. package/components/overlays/test/utils-tests/contain-focus.test.js +0 -236
  80. package/dist-types/components/overlays/src/utils/contain-focus.d.ts +0 -20
  81. package/dist-types/components/overlays/src/utils/contain-focus.d.ts.map +0 -1
  82. package/dist-types/components/overlays/test/utils-tests/contain-focus.test.d.ts +0 -2
  83. package/dist-types/components/overlays/test/utils-tests/contain-focus.test.d.ts.map +0 -1
@@ -54,12 +54,10 @@ export class LionCollapsible extends LitElement {
54
54
  if (this._invokerNode) {
55
55
  this._invokerNode.addEventListener('click', this.toggle);
56
56
  this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
57
- this._invokerNode.setAttribute('id', `collapsible-invoker-${uid}`);
58
57
  this._invokerNode.setAttribute('aria-controls', `collapsible-content-${uid}`);
59
58
  }
60
59
 
61
60
  if (this._contentNode) {
62
- this._contentNode.setAttribute('aria-labelledby', `collapsible-invoker-${uid}`);
63
61
  this._contentNode.setAttribute('id', `collapsible-content-${uid}`);
64
62
  }
65
63
 
@@ -136,14 +136,5 @@ describe('<lion-collapsible>', () => {
136
136
  expect(invoker).to.have.attribute('aria-expanded', 'true');
137
137
  });
138
138
  });
139
-
140
- describe('Contents', () => {
141
- it('adds aria-labelledby referring to invoker id', async () => {
142
- const collapsibleElement = await fixture(defaultCollapsible);
143
- const invoker = collapsibleElement.querySelector('[slot=invoker]');
144
- const content = collapsibleElement.querySelector('[slot=content]');
145
- expect(content).to.have.attribute('aria-labelledby', invoker?.id);
146
- });
147
- });
148
139
  });
149
140
  });
@@ -0,0 +1 @@
1
+ // ... empty file needed for '@lit-labs/testing/fixtures.js'
@@ -96,14 +96,5 @@ describe('<lion-drawer>', () => {
96
96
  expect(invoker).to.have.attribute('aria-expanded', 'true');
97
97
  });
98
98
  });
99
-
100
- describe('Contents', () => {
101
- it('adds aria-labelledby referring to invoker id', async () => {
102
- const drawerElement = await fixture(template);
103
- const invoker = drawerElement.querySelector('[slot=invoker]');
104
- const content = drawerElement.querySelector('[slot=content]');
105
- expect(content).to.have.attribute('aria-labelledby', invoker?.id);
106
- });
107
- });
108
99
  });
109
100
  });
@@ -1,6 +1,6 @@
1
1
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
2
- import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
3
2
  import { InteractionStateMixin } from '../InteractionStateMixin.js';
3
+ import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
4
4
  import { ValidateMixin } from '../validate/ValidateMixin.js';
5
5
 
6
6
  /**
@@ -260,7 +260,17 @@ const ChoiceGroupMixinImplementation = superclass =>
260
260
  ) {
261
261
  return this[property];
262
262
  }
263
- return this.formElements.filter(el => _filterFn(el, property)).map(el => el.property);
263
+
264
+ // TODO: consider a shared approach for all value getters
265
+ const filteredFnElements = this.formElements.filter(el => _filterFn(el, property));
266
+
267
+ if (property === '_initialModelValue') {
268
+ return this.multipleChoice
269
+ ? filteredFnElements.filter(el => el[property].checked).map(el => el[property].value)
270
+ : filteredFnElements.find(el => el[property].checked)?.value;
271
+ }
272
+
273
+ return filteredFnElements.map(el => el[property]);
264
274
  }
265
275
 
266
276
  /**
@@ -1,15 +1,15 @@
1
- import { LitElement } from 'lit';
2
- import '@lion/ui/define/lion-fieldset.js';
3
1
  import '@lion/ui/define/lion-checkbox-group.js';
4
2
  import '@lion/ui/define/lion-checkbox.js';
3
+ import '@lion/ui/define/lion-fieldset.js';
5
4
  import {
6
- FormGroupMixin,
7
- Required,
8
5
  ChoiceGroupMixin,
9
6
  ChoiceInputMixin,
7
+ FormGroupMixin,
8
+ Required,
10
9
  } from '@lion/ui/form-core.js';
11
10
  import { LionInput } from '@lion/ui/input.js';
12
11
  import { expect, fixture, fixtureSync, html, unsafeStatic } from '@open-wc/testing';
12
+ import { LitElement } from 'lit';
13
13
  import sinon from 'sinon';
14
14
 
15
15
  class ChoiceInputFoo extends ChoiceInputMixin(LionInput) {}
@@ -45,6 +45,28 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
45
45
 
46
46
  describe(`ChoiceGroupMixin: ${cfg.parentTagString}`, () => {
47
47
  if (cfg.choiceType === 'single') {
48
+ it('has a single modelValue representing initial checked values', async () => {
49
+ const el = /** @type {ChoiceInputGroup} */ (
50
+ await fixture(html`
51
+ <${parentTag} name="gender[]">
52
+ <${childTag} .choiceValue=${'male'}></${childTag}>
53
+ <${childTag} .choiceValue=${'female'} checked></${childTag}>
54
+ <${childTag} .choiceValue=${'other'}></${childTag}>
55
+ </${parentTag}>
56
+ `)
57
+ );
58
+
59
+ // @ts-ignore protected members allowed in test
60
+ expect(el._initialModelValue).to.eql('female');
61
+ expect(el.modelValue).to.eql('female');
62
+
63
+ el.formElements[0].checked = true;
64
+
65
+ // @ts-ignore protected members allowed in test
66
+ expect(el._initialModelValue).to.eql('female');
67
+ expect(el.modelValue).to.eql('male');
68
+ });
69
+
48
70
  it('has a single modelValue representing the currently checked radio value', async () => {
49
71
  const el = /** @type {ChoiceInputGroup} */ (
50
72
  await fixture(html`
@@ -585,6 +607,28 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
585
607
  });
586
608
 
587
609
  describe('multipleChoice', () => {
610
+ it('has multiple _initialModelValue representing initial checked values', async () => {
611
+ const el = /** @type {ChoiceInputGroup} */ (
612
+ await fixture(html`
613
+ <${parentTag} multiple-choice name="animals[]">
614
+ <${childTag} .choiceValue=${'dog'} checked></${childTag}>
615
+ <${childTag} .choiceValue=${'cat'} checked></${childTag}>
616
+ <${childTag} .choiceValue=${'lion'}></${childTag}>
617
+ </${parentTag}>
618
+ `)
619
+ );
620
+
621
+ // @ts-ignore protected members allowed in test
622
+ expect(el._initialModelValue).to.eql(['dog', 'cat']);
623
+ expect(el.modelValue).to.eql(['dog', 'cat']);
624
+
625
+ el.formElements[2].checked = true;
626
+
627
+ // @ts-ignore protected members allowed in test
628
+ expect(el._initialModelValue).to.eql(['dog', 'cat']);
629
+ expect(el.modelValue).to.eql(['dog', 'cat', 'lion']);
630
+ });
631
+
588
632
  it('has a single modelValue representing all currently checked values', async () => {
589
633
  const el = /** @type {ChoiceInputGroup} */ (
590
634
  await fixture(html`
@@ -1,5 +1,5 @@
1
1
  export default {
2
- allCountries: 'Pays suggérés',
2
+ allCountries: 'Todos los países',
3
3
  phoneNumber: 'Número de teléfono',
4
4
  selectCountry: 'Seleccione país',
5
5
  suggestedCountries: 'Países sugeridos',
@@ -1,8 +1,8 @@
1
1
  import { overlays } from './singleton.js';
2
- import { containFocus } from './utils/contain-focus.js';
3
2
  import { deepContains } from './utils/deep-contains.js';
4
3
  import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
5
4
  import { _adoptStyleUtils } from './utils/adopt-styles.js';
5
+ import { getFocusableElements } from './utils/get-focusable-elements.js';
6
6
 
7
7
  /**
8
8
  * @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
@@ -113,6 +113,11 @@ const childDialogsClosedInEventLoopWeakmap = new WeakMap();
113
113
  *
114
114
  */
115
115
  export class OverlayController extends EventTarget {
116
+ /**
117
+ * 'True' when Shift key is pressed, 'false' otherwise
118
+ */
119
+ #isShiftPressed = false;
120
+
116
121
  /**
117
122
  * @constructor
118
123
  * @param {OverlayConfig} config initial config. Will be remembered as shared config
@@ -201,8 +206,6 @@ export class OverlayController extends EventTarget {
201
206
  this.__escKeyHandler = this.__escKeyHandler.bind(this);
202
207
  this.updateConfig(config);
203
208
  /** @private */
204
- this.__hasActiveTrapsKeyboardFocus = false;
205
- /** @private */
206
209
  this.__hasActiveBackdrop = true;
207
210
  /** @private */
208
211
  this.__cancelHandler = this.__cancelHandler.bind(this);
@@ -591,9 +594,7 @@ export class OverlayController extends EventTarget {
591
594
  * @private
592
595
  */
593
596
  __initContentDomStructure() {
594
- const wrappingDialogElement = document.createElement(
595
- this.config?._noDialogEl ? 'div' : 'dialog',
596
- );
597
+ const wrappingDialogElement = document.createElement('dialog');
597
598
  // We use a dialog for its visual capabilities: it renders to the top layer.
598
599
  // A11y will depend on the type of overlay and is arranged on contentNode level.
599
600
  // Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
@@ -625,7 +626,6 @@ export class OverlayController extends EventTarget {
625
626
  contentWrapperNodeL2: this.contentWrapperNode,
626
627
  contentNodeL3: this.contentNode,
627
628
  });
628
- // @ts-expect-error
629
629
  wrappingDialogElement.open = true;
630
630
 
631
631
  if (this.isTooltip) {
@@ -1145,65 +1145,97 @@ export class OverlayController extends EventTarget {
1145
1145
  }
1146
1146
  }
1147
1147
 
1148
- get hasActiveTrapsKeyboardFocus() {
1149
- return this.__hasActiveTrapsKeyboardFocus;
1150
- }
1151
-
1152
1148
  /**
1153
- * @param {{ phase: OverlayPhase }} config
1154
- * @protected
1149
+ * @param {KeyboardEvent} event
1155
1150
  */
1156
- _handleTrapsKeyboardFocus({ phase }) {
1157
- if (phase === 'show') {
1158
- // @ts-ignore
1159
- if ('showModal' in this.__wrappingDialogNode) {
1160
- // @ts-ignore
1161
- this.__wrappingDialogNode.close();
1162
- // @ts-ignore
1163
- this.__wrappingDialogNode.showModal();
1164
- }
1165
- // else {
1166
- this.enableTrapsKeyboardFocus();
1167
- // }
1168
- } else if (phase === 'hide' || phase === 'teardown') {
1169
- this.disableTrapsKeyboardFocus();
1151
+ #isShiftPressedOnKeyDownHandler = event => {
1152
+ if (event.key === 'Shift') {
1153
+ this.#isShiftPressed = true;
1170
1154
  }
1171
- }
1155
+ };
1172
1156
 
1173
- enableTrapsKeyboardFocus() {
1174
- if (this.__hasActiveTrapsKeyboardFocus) {
1175
- return;
1176
- }
1177
- if (this.manager) {
1178
- this.manager.disableTrapsKeyboardFocusForAll();
1157
+ /**
1158
+ * @param {KeyboardEvent} event
1159
+ */
1160
+ #isShiftPressedOnKeyUpHandler = event => {
1161
+ if (event.key === 'Shift') {
1162
+ this.#isShiftPressed = false;
1179
1163
  }
1164
+ };
1180
1165
 
1181
- const isContentShadowHost = Boolean(this.contentNode.shadowRoot);
1182
- if (isContentShadowHost) {
1183
- // eslint-disable-next-line no-console
1184
- console.warn(
1185
- '[overlays]: For best accessibility (compatibility with Safari + VoiceOver), provide a contentNode that is not a host for a shadow root',
1186
- );
1187
- }
1166
+ #handleShiftKeyPress = () => {
1167
+ window.addEventListener('keydown', this.#isShiftPressedOnKeyDownHandler);
1168
+ window.addEventListener('keyup', this.#isShiftPressedOnKeyUpHandler);
1169
+ };
1188
1170
 
1189
- this._containFocusHandler = containFocus(this.contentNode);
1190
- this.__hasActiveTrapsKeyboardFocus = true;
1191
- if (this.manager) {
1192
- this.manager.informTrapsKeyboardFocusGotEnabled(this.placementMode);
1193
- }
1194
- }
1171
+ #stopHandlingShiftKeyPress = () => {
1172
+ window.removeEventListener('keydown', this.#isShiftPressedOnKeyDownHandler);
1173
+ window.removeEventListener('keyup', this.#isShiftPressedOnKeyUpHandler);
1174
+ };
1195
1175
 
1196
- disableTrapsKeyboardFocus({ findNewTrap = true } = {}) {
1197
- if (!this.__hasActiveTrapsKeyboardFocus) {
1198
- return;
1176
+ #getInitialElementToFocus = () => {
1177
+ const focusableElements = getFocusableElements(this.contentNode);
1178
+ // Initial focus goes to first element with autofocus, or `contentNode`
1179
+ return focusableElements.find(e => e.hasAttribute('autofocus')) || this.contentNode;
1180
+ };
1181
+
1182
+ /**
1183
+ * When a `dialog` element gets focused, we focus programmatically something
1184
+ * else inside dialog for better a11y. A dialog element gets focused natively in these cases:
1185
+ * 1) When called by `showModal()` first time
1186
+ * 2) When focus is rotating. That is when a user navigates using Tab key through
1187
+ * all the dialog's focusable elements, then the focus goes to the browser's URL,
1188
+ * all its tabs and then the focus goes back to the dialog element
1189
+ * 3) Same as in the point #2, but when a user navigates backward by hitting `Shift + Tab`.
1190
+ * In this case we do not intercept and let the focus pass through. Otherwise the focus
1191
+ * will never leaves the dialog
1192
+ *
1193
+ * Note, Chrome does not focus `Dialog` element when Tabbing. When dialog is opened first time,
1194
+ * it focuses the contentNode if that has `tabindex` set. But the second time when we
1195
+ * move to the dialog from URL bar, nor the dialog element, nor the `contentNode` are focused.
1196
+ * Instead the first focusable element is focused right away
1197
+ */
1198
+ #handleFocusInsideDialog = () => {
1199
+ this.__wrappingDialogNode?.addEventListener('focus', () => {
1200
+ if (!this.#isShiftPressed) {
1201
+ this.#getInitialElementToFocus().focus();
1202
+ }
1203
+ });
1204
+ };
1205
+
1206
+ /**
1207
+ * @param {{ phase: OverlayPhase }} config
1208
+ * @protected
1209
+ */
1210
+ _handleTrapsKeyboardFocus({ phase }) {
1211
+ if (phase === 'init') {
1212
+ this.contentNode.style.outline = 'none';
1213
+ this.contentNode.tabIndex = -1;
1214
+
1215
+ const isContentShadowHost = Boolean(this.contentNode.shadowRoot);
1216
+ if (isContentShadowHost) {
1217
+ // eslint-disable-next-line no-console
1218
+ console.warn(
1219
+ '[overlays]: For best accessibility (compatibility with Safari + VoiceOver), provide a contentNode that is not a host for a shadow root',
1220
+ );
1221
+ }
1199
1222
  }
1200
- if (this._containFocusHandler) {
1201
- this._containFocusHandler.disconnect();
1202
- this._containFocusHandler = undefined;
1223
+ if (phase === 'show') {
1224
+ this.#handleShiftKeyPress();
1225
+ this.#handleFocusInsideDialog();
1226
+ this.__wrappingDialogNode?.close();
1227
+ this.__wrappingDialogNode?.showModal();
1228
+ /**
1229
+ * At this moment `#handleFocusInsideDialog` should handle the focus.
1230
+ * But for some reason Firefox on the testing setup does not
1231
+ * focus the native `dialog` on showModal() and focuses the first
1232
+ * focusable element inside the dialog instead. Hence here we focus
1233
+ * contentNode explicitly
1234
+ */
1235
+ this.#getInitialElementToFocus().focus();
1203
1236
  }
1204
- this.__hasActiveTrapsKeyboardFocus = false;
1205
- if (this.manager) {
1206
- this.manager.informTrapsKeyboardFocusGotDisabled({ disabledCtrl: this, findNewTrap });
1237
+ if (phase === 'hide') {
1238
+ this.#stopHandlingShiftKeyPress();
1207
1239
  }
1208
1240
  }
1209
1241
 
@@ -1231,12 +1263,7 @@ export class OverlayController extends EventTarget {
1231
1263
  return;
1232
1264
  }
1233
1265
 
1234
- const hasPressedInside =
1235
- event.composedPath().includes(this.contentNode) ||
1236
- (this.invokerNode && event.composedPath().includes(this.invokerNode)) ||
1237
- deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1238
-
1239
- if (hasPressedInside) {
1266
+ if (this.#hasPressedInside(event)) {
1240
1267
  this.__escKeyHandlerCalled = true;
1241
1268
  this.hide();
1242
1269
  // We could do event.stopPropagation() here, but we don't want to hide info for
@@ -1246,6 +1273,15 @@ export class OverlayController extends EventTarget {
1246
1273
  }
1247
1274
  }
1248
1275
 
1276
+ /**
1277
+ * @param {KeyboardEvent} event
1278
+ * @returns {boolean}
1279
+ */
1280
+ #hasPressedInside = event =>
1281
+ event.composedPath().includes(/** @type {EventTarget} */ (this.__wrappingDialogNode)) ||
1282
+ (this.invokerNode && event.composedPath().includes(this.invokerNode)) ||
1283
+ deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1284
+
1249
1285
  /**
1250
1286
  * @param {KeyboardEvent} event
1251
1287
  * @returns {void}
@@ -1253,10 +1289,7 @@ export class OverlayController extends EventTarget {
1253
1289
  #outsideEscKeyHandler = event => {
1254
1290
  if (event.key !== 'Escape') return;
1255
1291
 
1256
- const hasPressedInside =
1257
- event.composedPath().includes(this.contentNode) ||
1258
- deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1259
- if (hasPressedInside) return;
1292
+ if (this.#hasPressedInside(event)) return;
1260
1293
  this.hide();
1261
1294
  };
1262
1295
 
@@ -134,39 +134,6 @@ export class OverlaysManager {
134
134
  return this.__siblingsInert;
135
135
  }
136
136
 
137
- disableTrapsKeyboardFocusForAll() {
138
- this.shownList.forEach(ctrl => {
139
- if (ctrl.trapsKeyboardFocus === true && ctrl.disableTrapsKeyboardFocus) {
140
- ctrl.disableTrapsKeyboardFocus({ findNewTrap: false });
141
- }
142
- });
143
- }
144
-
145
- /**
146
- * @param {'local' | 'global' | undefined} placementMode
147
- */
148
- informTrapsKeyboardFocusGotEnabled(placementMode) {
149
- if (this.siblingsInert === false && placementMode === 'global') {
150
- this.__siblingsInert = true;
151
- }
152
- }
153
-
154
- /**
155
- * @param {{ disabledCtrl?:OverlayController, findNewTrap?:boolean }} options
156
- */
157
- informTrapsKeyboardFocusGotDisabled({ disabledCtrl, findNewTrap = true } = {}) {
158
- const next = this.shownList.find(
159
- ctrl => ctrl !== disabledCtrl && ctrl.trapsKeyboardFocus === true,
160
- );
161
- if (next) {
162
- if (findNewTrap) {
163
- next.enableTrapsKeyboardFocus();
164
- }
165
- } else if (this.siblingsInert === true) {
166
- this.__siblingsInert = false;
167
- }
168
- }
169
-
170
137
  /** PreventsScroll */
171
138
 
172
139
  // eslint-disable-next-line class-methods-use-this
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable no-new */
2
+ /* eslint-disable lit-a11y/no-autofocus */
2
3
  import { OverlayController, overlays } from '@lion/ui/overlays.js';
3
4
  import { mimicClick } from '@lion/ui/overlays-test-helpers.js';
4
5
  import { sendKeys } from '@web/test-runner-commands';
@@ -11,13 +12,14 @@ import {
11
12
  fixture,
12
13
  expect,
13
14
  html,
15
+ waitUntil,
14
16
  } from '@open-wc/testing';
15
17
 
16
18
  import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
17
19
  import { createShadowHost } from '../test-helpers/createShadowHost.js';
18
20
  import { _adoptStyleUtils } from '../src/utils/adopt-styles.js';
19
21
  import { simulateTab } from '../src/utils/simulate-tab.js';
20
- import { keyCodes } from '../src/utils/key-codes.js';
22
+ import { browserDetection } from '../../core/src/browserDetection.js';
21
23
 
22
24
  /**
23
25
  * @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
@@ -584,30 +586,57 @@ describe('OverlayController', () => {
584
586
 
585
587
  describe('Feature Configuration', () => {
586
588
  describe('trapsKeyboardFocus', () => {
587
- it('offers an hasActiveTrapsKeyboardFocus flag', async () => {
589
+ it('focuses the overlay on show', async () => {
588
590
  const ctrl = new OverlayController({
589
591
  ...withGlobalTestConfig(),
590
592
  trapsKeyboardFocus: true,
591
593
  });
592
- expect(ctrl.hasActiveTrapsKeyboardFocus).to.be.false;
593
-
594
594
  await ctrl.show();
595
- expect(ctrl.hasActiveTrapsKeyboardFocus).to.be.true;
595
+ expect(isActiveElement(ctrl.contentNode)).to.be.true;
596
596
  });
597
597
 
598
- it('focuses the overlay on show', async () => {
598
+ it('keeps focus within the overlay e.g. you can not tab out by accident', async () => {
599
+ const contentNode = /** @type {HTMLElement} */ (
600
+ await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
601
+ );
599
602
  const ctrl = new OverlayController({
600
603
  ...withGlobalTestConfig(),
601
604
  trapsKeyboardFocus: true,
605
+ contentNode,
602
606
  });
603
607
  await ctrl.show();
604
608
 
605
- expect(isActiveElement(ctrl.contentNode)).to.be.true;
609
+ await fixture(html`<button>click me</button>`);
610
+ const input1 = ctrl.contentNode.querySelectorAll('input')[0];
611
+ const input2 = ctrl.contentNode.querySelectorAll('input')[1];
612
+ input2.focus();
613
+ await sendKeys({ press: 'Tab' });
614
+
615
+ if (browserDetection.isChrome) {
616
+ expect(isActiveElement(document.body)).to.be.true;
617
+ await sendKeys({ press: 'Tab' });
618
+ expect(isActiveElement(input1)).to.be.true;
619
+ }
620
+ if (browserDetection.isFirefox) {
621
+ // For some reason with the headless firefox setup,
622
+ // once we focus on the latest focusable element, `input2` in this case,
623
+ // any further Tab presses keeps the focus on `input2` element
624
+ expect(isActiveElement(input2)).to.be.true;
625
+ await sendKeys({ press: 'Tab' });
626
+ expect(isActiveElement(input2)).to.be.true;
627
+ }
628
+ if (browserDetection.isMacSafari) {
629
+ expect(isActiveElement(document.body)).to.be.true;
630
+ await sendKeys({ press: 'Tab' });
631
+ expect(isActiveElement(ctrl.contentNode)).to.be.true;
632
+ await sendKeys({ press: 'Tab' });
633
+ expect(isActiveElement(input1)).to.be.true;
634
+ }
606
635
  });
607
636
 
608
- it('keeps focus within the overlay e.g. you can not tab out by accident', async () => {
637
+ it('focuses the element with [autofocus] when dialog gets opened', async () => {
609
638
  const contentNode = /** @type {HTMLElement} */ (
610
- await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
639
+ await fixture(html` <div><input id="input1" /><input id="input2" autofocus /></div> `)
611
640
  );
612
641
  const ctrl = new OverlayController({
613
642
  ...withGlobalTestConfig(),
@@ -616,21 +645,21 @@ describe('OverlayController', () => {
616
645
  });
617
646
  await ctrl.show();
618
647
 
619
- const elOutside = /** @type {HTMLElement} */ (
620
- await fixture(html`<button>click me</button>`)
621
- );
622
- const input1 = ctrl.contentNode.querySelectorAll('input')[0];
623
648
  const input2 = ctrl.contentNode.querySelectorAll('input')[1];
649
+ expect(isActiveElement(input2)).to.be.true;
650
+ });
624
651
 
625
- input2.focus();
626
- // this mimics a tab within the contain-focus system used
627
- const event = new CustomEvent('keydown', { detail: 0, bubbles: true });
628
- // @ts-ignore override private key
629
- event.keyCode = keyCodes.tab;
630
- window.dispatchEvent(event);
631
-
632
- expect(isActiveElement(elOutside)).to.be.false;
633
- expect(isActiveElement(input1)).to.be.true;
652
+ it('focuses the contentNode when dialog gets opened and there is no [autofocus]', async () => {
653
+ const contentNode = /** @type {HTMLElement} */ (
654
+ await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
655
+ );
656
+ const ctrl = new OverlayController({
657
+ ...withGlobalTestConfig(),
658
+ trapsKeyboardFocus: true,
659
+ contentNode,
660
+ });
661
+ await ctrl.show();
662
+ expect(isActiveElement(contentNode)).to.be.true;
634
663
  });
635
664
 
636
665
  it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => {
@@ -667,12 +696,10 @@ describe('OverlayController', () => {
667
696
 
668
697
  await ctrl0.show();
669
698
  await ctrl1.show();
670
- expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.false;
671
- expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.true;
699
+ expect(isActiveElement(ctrl1.contentNode)).to.be.true;
672
700
 
673
701
  await ctrl1.hide();
674
- expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.true;
675
- expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.false;
702
+ expect(isActiveElement(ctrl0.contentNode)).to.be.true;
676
703
  });
677
704
 
678
705
  it('warns when contentNode is a host for a shadowRoot', async () => {
@@ -811,12 +838,8 @@ describe('OverlayController', () => {
811
838
  );
812
839
  const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
813
840
  await mimicEscapePress(childOverlay.contentNode);
814
-
815
- // without this line, the test is unstable on FF sometimes
816
- await aTimeout(100);
817
-
818
- expect(parentOverlay.isShown).to.be.false;
819
- expect(childOverlay.isShown).to.be.true;
841
+ await waitUntil(() => !parentOverlay.isShown);
842
+ await waitUntil(() => childOverlay.isShown);
820
843
 
821
844
  await childOverlay.teardown();
822
845
  await parentOverlay.teardown();