@lion/ui 0.15.5 → 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 (69) hide show
  1. package/components/overlays/src/OverlayController.js +99 -66
  2. package/components/overlays/src/OverlaysManager.js +0 -33
  3. package/components/overlays/test/OverlayController.test.js +52 -30
  4. package/custom-elements.json +105 -200
  5. package/dist-types/components/overlays/src/OverlayController.d.ts +1 -11
  6. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  7. package/dist-types/components/overlays/src/OverlaysManager.d.ts +0 -12
  8. package/dist-types/components/overlays/src/OverlaysManager.d.ts.map +1 -1
  9. package/dist-types/components/overlays/test/OverlayController.test.d.ts.map +1 -1
  10. package/dist-types/exports/overlays.d.ts +0 -1
  11. package/dist-types/tsconfig.tsbuildinfo +1 -1
  12. package/docs/components/checkbox-group/overview.md +2 -2
  13. package/docs/components/collapsible/overview.md +1 -1
  14. package/docs/components/combobox/overview.md +1 -1
  15. package/docs/components/combobox/use-cases.md +1 -1
  16. package/docs/components/dialog/overview.md +1 -1
  17. package/docs/components/fieldset/overview.md +5 -5
  18. package/docs/components/form/overview.md +3 -3
  19. package/docs/components/form/use-cases.md +2 -2
  20. package/docs/components/icon/use-cases.md +1 -1
  21. package/docs/components/input/overview.md +1 -1
  22. package/docs/components/input-amount/overview.md +3 -3
  23. package/docs/components/input-date/overview.md +3 -3
  24. package/docs/components/input-datepicker/overview.md +5 -5
  25. package/docs/components/input-email/overview.md +2 -2
  26. package/docs/components/input-file/overview.md +1 -1
  27. package/docs/components/input-iban/overview.md +2 -2
  28. package/docs/components/input-range/overview.md +2 -2
  29. package/docs/components/input-stepper/overview.md +1 -1
  30. package/docs/components/input-tel/overview.md +1 -1
  31. package/docs/components/input-tel/use-cases.md +6 -6
  32. package/docs/components/input-tel-dropdown/overview.md +1 -1
  33. package/docs/components/listbox/use-cases.md +1 -1
  34. package/docs/components/radio-group/overview.md +3 -3
  35. package/docs/components/select/use-cases.md +1 -1
  36. package/docs/components/select-rich/use-cases.md +2 -2
  37. package/docs/components/tabs/examples.md +1 -1
  38. package/docs/fundamentals/index.md +4 -4
  39. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  40. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  41. package/docs/fundamentals/rationales/accessibility.md +1 -1
  42. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  43. package/docs/fundamentals/systems/core/overview.md +1 -1
  44. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  45. package/docs/fundamentals/systems/form/model-value.md +1 -1
  46. package/docs/fundamentals/systems/form/overview.md +28 -28
  47. package/docs/fundamentals/systems/form/validate.md +2 -2
  48. package/docs/fundamentals/systems/localize/overview.md +4 -4
  49. package/docs/fundamentals/systems/overlays/api-table.md +45 -51
  50. package/docs/fundamentals/systems/overlays/configuration.md +5 -2
  51. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  52. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  53. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  54. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  55. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  56. package/docs/guides/how-to/create-a-form.md +4 -4
  57. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  58. package/docs/guides/index.md +8 -8
  59. package/docs/guides/principles/index.md +5 -5
  60. package/docs/guides/principles/styling.md +1 -1
  61. package/exports/overlays.js +0 -1
  62. package/overlays.js +0 -1
  63. package/package.json +1 -1
  64. package/components/overlays/src/utils/contain-focus.js +0 -202
  65. package/components/overlays/test/utils-tests/contain-focus.test.js +0 -236
  66. package/dist-types/components/overlays/src/utils/contain-focus.d.ts +0 -20
  67. package/dist-types/components/overlays/src/utils/contain-focus.d.ts.map +0 -1
  68. package/dist-types/components/overlays/test/utils-tests/contain-focus.test.d.ts +0 -2
  69. package/dist-types/components/overlays/test/utils-tests/contain-focus.test.d.ts.map +0 -1
@@ -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';
@@ -18,7 +19,7 @@ import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
18
19
  import { createShadowHost } from '../test-helpers/createShadowHost.js';
19
20
  import { _adoptStyleUtils } from '../src/utils/adopt-styles.js';
20
21
  import { simulateTab } from '../src/utils/simulate-tab.js';
21
- import { keyCodes } from '../src/utils/key-codes.js';
22
+ import { browserDetection } from '../../core/src/browserDetection.js';
22
23
 
23
24
  /**
24
25
  * @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
@@ -585,30 +586,57 @@ describe('OverlayController', () => {
585
586
 
586
587
  describe('Feature Configuration', () => {
587
588
  describe('trapsKeyboardFocus', () => {
588
- it('offers an hasActiveTrapsKeyboardFocus flag', async () => {
589
+ it('focuses the overlay on show', async () => {
589
590
  const ctrl = new OverlayController({
590
591
  ...withGlobalTestConfig(),
591
592
  trapsKeyboardFocus: true,
592
593
  });
593
- expect(ctrl.hasActiveTrapsKeyboardFocus).to.be.false;
594
-
595
594
  await ctrl.show();
596
- expect(ctrl.hasActiveTrapsKeyboardFocus).to.be.true;
595
+ expect(isActiveElement(ctrl.contentNode)).to.be.true;
597
596
  });
598
597
 
599
- 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
+ );
600
602
  const ctrl = new OverlayController({
601
603
  ...withGlobalTestConfig(),
602
604
  trapsKeyboardFocus: true,
605
+ contentNode,
603
606
  });
604
607
  await ctrl.show();
605
608
 
606
- 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
+ }
607
635
  });
608
636
 
609
- 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 () => {
610
638
  const contentNode = /** @type {HTMLElement} */ (
611
- await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
639
+ await fixture(html` <div><input id="input1" /><input id="input2" autofocus /></div> `)
612
640
  );
613
641
  const ctrl = new OverlayController({
614
642
  ...withGlobalTestConfig(),
@@ -617,21 +645,21 @@ describe('OverlayController', () => {
617
645
  });
618
646
  await ctrl.show();
619
647
 
620
- const elOutside = /** @type {HTMLElement} */ (
621
- await fixture(html`<button>click me</button>`)
622
- );
623
- const input1 = ctrl.contentNode.querySelectorAll('input')[0];
624
648
  const input2 = ctrl.contentNode.querySelectorAll('input')[1];
649
+ expect(isActiveElement(input2)).to.be.true;
650
+ });
625
651
 
626
- input2.focus();
627
- // this mimics a tab within the contain-focus system used
628
- const event = new CustomEvent('keydown', { detail: 0, bubbles: true });
629
- // @ts-ignore override private key
630
- event.keyCode = keyCodes.tab;
631
- window.dispatchEvent(event);
632
-
633
- expect(isActiveElement(elOutside)).to.be.false;
634
- 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;
635
663
  });
636
664
 
637
665
  it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => {
@@ -668,12 +696,10 @@ describe('OverlayController', () => {
668
696
 
669
697
  await ctrl0.show();
670
698
  await ctrl1.show();
671
- expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.false;
672
- expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.true;
699
+ expect(isActiveElement(ctrl1.contentNode)).to.be.true;
673
700
 
674
701
  await ctrl1.hide();
675
- expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.true;
676
- expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.false;
702
+ expect(isActiveElement(ctrl0.contentNode)).to.be.true;
677
703
  });
678
704
 
679
705
  it('warns when contentNode is a host for a shadowRoot', async () => {
@@ -812,10 +838,6 @@ describe('OverlayController', () => {
812
838
  );
813
839
  const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
814
840
  await mimicEscapePress(childOverlay.contentNode);
815
-
816
- // without this line, the test is unstable on FF sometimes
817
- await aTimeout(100);
818
-
819
841
  await waitUntil(() => !parentOverlay.isShown);
820
842
  await waitUntil(() => childOverlay.isShown);
821
843