@lion/ui 0.9.1 → 0.10.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.
- package/components/button/src/LionButton.js +5 -1
- package/components/button/test-suites/LionButton.suite.js +1 -1
- package/components/calendar/src/LionCalendar.js +5 -14
- package/components/calendar/test/lion-calendar.test.js +88 -0
- package/components/calendar/translations/bg.js +5 -0
- package/components/calendar/translations/cs.js +5 -0
- package/components/calendar/translations/de.js +5 -0
- package/components/calendar/translations/en.js +5 -0
- package/components/calendar/translations/es.js +5 -0
- package/components/calendar/translations/fr.js +5 -0
- package/components/calendar/translations/hu.js +5 -0
- package/components/calendar/translations/it.js +5 -0
- package/components/calendar/translations/nl.js +5 -0
- package/components/calendar/translations/pl.js +5 -0
- package/components/calendar/translations/ro.js +5 -0
- package/components/calendar/translations/ru.js +5 -0
- package/components/calendar/translations/sk.js +5 -0
- package/components/calendar/translations/uk.js +3 -0
- package/components/calendar/translations/zh.js +3 -0
- package/components/combobox/test/lion-combobox.test.js +2 -0
- package/components/core/src/SlotMixin.js +15 -0
- package/components/core/test/ScopedElementsMixin.test.js +3 -3
- package/components/core/test/SlotMixin.test.js +50 -0
- package/components/dialog/src/LionDialog.js +13 -0
- package/components/dialog/test/lion-dialog.test.js +110 -0
- package/components/dialog/test-helpers/test-router.js +43 -0
- package/components/form-core/src/FormatMixin.js +66 -18
- package/components/form-core/src/choice-group/ChoiceInputMixin.js +3 -1
- package/components/form-core/test-suites/FormatMixin.suite.js +107 -8
- package/components/form-core/types/FormatMixinTypes.ts +4 -1
- package/components/form-integrations/test/dialog-integrations.test.js +6 -2
- package/components/input-amount/src/parsers.js +3 -3
- package/components/input-amount/test/lion-input-amount.test.js +36 -2
- package/components/input-amount/test/parsers.test.js +11 -0
- package/components/input-tel-dropdown/src/LionInputTelDropdown.js +1 -1
- package/components/localize/src/number/getDecimalSeparator.js +1 -1
- package/components/localize/src/number/parseNumber.js +9 -6
- package/components/localize/test/number/parseNumber.test.js +11 -0
- package/components/localize/types/LocalizeMixinTypes.ts +3 -3
- package/components/overlays/src/OverlayController.js +21 -6
- package/components/overlays/src/OverlayMixin.js +20 -14
- package/components/overlays/test/OverlayController.test.js +40 -8
- package/components/overlays/test-suites/OverlayMixin.suite.js +184 -9
- package/components/overlays/types/ArrowMixinTypes.ts +11 -2
- package/components/overlays/types/OverlayConfig.ts +19 -15
- package/components/select-rich/src/LionSelectRich.js +3 -1
- package/components/select-rich/test/lion-select-rich.test.js +44 -0
- package/custom-elements.json +1340 -89
- package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
- package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
- package/dist-types/components/calendar/translations/bg.d.ts +3 -0
- package/dist-types/components/calendar/translations/cs.d.ts +3 -0
- package/dist-types/components/calendar/translations/de.d.ts +3 -0
- package/dist-types/components/calendar/translations/en.d.ts +3 -0
- package/dist-types/components/calendar/translations/es.d.ts +3 -0
- package/dist-types/components/calendar/translations/fr.d.ts +3 -0
- package/dist-types/components/calendar/translations/hu.d.ts +3 -0
- package/dist-types/components/calendar/translations/it.d.ts +3 -0
- package/dist-types/components/calendar/translations/nl.d.ts +3 -0
- package/dist-types/components/calendar/translations/pl.d.ts +3 -0
- package/dist-types/components/calendar/translations/ro.d.ts +3 -0
- package/dist-types/components/calendar/translations/ru.d.ts +3 -0
- package/dist-types/components/calendar/translations/sk.d.ts +3 -0
- package/dist-types/components/calendar/translations/uk.d.ts +3 -0
- package/dist-types/components/calendar/translations/zh.d.ts +3 -0
- package/dist-types/components/core/src/SlotMixin.d.ts.map +1 -1
- package/dist-types/components/dialog/src/LionDialog.d.ts +32 -0
- package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
- package/dist-types/components/dialog/test/lion-dialog.test.d.ts.map +1 -1
- package/dist-types/components/dialog/test-helpers/test-router.d.ts +2 -0
- package/dist-types/components/dialog/test-helpers/test-router.d.ts.map +1 -0
- package/dist-types/components/form-core/src/FormatMixin.d.ts +1 -1
- package/dist-types/components/form-core/src/FormatMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/src/choice-group/ChoiceInputMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts.map +1 -1
- package/dist-types/components/form-core/types/FormatMixinTypes.d.ts +3 -2
- package/dist-types/components/form-core/types/FormatMixinTypes.d.ts.map +1 -1
- package/dist-types/components/form-integrations/test/dialog-integrations.test.d.ts.map +1 -1
- package/dist-types/components/input-amount/src/parsers.d.ts +2 -2
- package/dist-types/components/input-amount/src/parsers.d.ts.map +1 -1
- package/dist-types/components/input-amount/test/lion-input-amount.test.d.ts.map +1 -1
- package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +1 -1
- package/dist-types/components/localize/src/number/parseNumber.d.ts.map +1 -1
- package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts +3 -2
- package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayController.d.ts +5 -0
- package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
- package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts +4 -4
- package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
- package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts +2 -2
- package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
- package/dist-types/components/overlays/types/OverlayConfig.d.ts +8 -6
- package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts +2 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
- package/dist-types/components/select-rich/test/lion-select-rich.test.d.ts.map +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/api-table.md +2 -2
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/api-table.md +1 -0
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/api-table.md +6 -3
- package/docs/components/dialog/overview.md +1 -1
- package/docs/components/dialog/use-cases.md +32 -1
- package/docs/components/fieldset/overview.md +5 -5
- package/docs/components/form/overview.md +3 -3
- package/docs/components/form/use-cases.md +2 -2
- package/docs/components/icon/use-cases.md +1 -1
- package/docs/components/input/api-table.md +1 -1
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/api-table.md +1 -1
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-date/api-table.md +1 -1
- package/docs/components/input-date/overview.md +3 -3
- package/docs/components/input-datepicker/api-table.md +2 -1
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/api-table.md +1 -1
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/api-table.md +1 -1
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-iban/api-table.md +1 -1
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/api-table.md +1 -1
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-stepper/api-table.md +1 -1
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-tel/api-table.md +1 -1
- package/docs/components/input-tel/overview.md +1 -1
- package/docs/components/input-tel/use-cases.md +6 -6
- package/docs/components/input-tel-dropdown/api-table.md +1 -1
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/api-table.md +1 -1
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/radio-group/api-table.md +1 -1
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/api-table.md +1 -1
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/api-table.md +1 -0
- package/docs/components/select-rich/use-cases.md +1 -1
- package/docs/components/switch/api-table.md +1 -1
- package/docs/components/tabs/examples.md +1 -1
- package/docs/components/textarea/api-table.md +1 -1
- package/docs/components/tooltip/api-table.md +1 -0
- package/docs/fundamentals/index.md +4 -4
- package/docs/fundamentals/node-tools/providence-analytics/overview.md +5 -5
- package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
- package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
- package/docs/fundamentals/rationales/accessibility.md +1 -1
- package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
- package/docs/fundamentals/systems/core/overview.md +1 -1
- package/docs/fundamentals/systems/form/api-table.md +1 -1
- package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
- package/docs/fundamentals/systems/form/model-value.md +1 -1
- package/docs/fundamentals/systems/form/overview.md +28 -28
- package/docs/fundamentals/systems/form/validate.md +2 -2
- package/docs/fundamentals/systems/localize/overview.md +4 -4
- package/docs/fundamentals/systems/overlays/api-table.md +35 -34
- package/docs/fundamentals/systems/overlays/configuration.md +32 -2
- package/docs/fundamentals/systems/overlays/overview.md +5 -5
- package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
- package/docs/fundamentals/tools/helpers/overview.md +1 -1
- package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
- package/docs/guides/how-to/create-a-custom-field.md +5 -5
- package/docs/guides/how-to/create-a-form.md +10 -9
- package/docs/guides/how-to/extend-a-native-input.md +2 -2
- package/docs/guides/index.md +8 -8
- package/docs/guides/principles/index.md +5 -5
- package/docs/guides/principles/styling.md +1 -1
- package/package.json +1 -1
|
@@ -151,6 +151,7 @@ export class OverlayController extends EventTarget {
|
|
|
151
151
|
hidesOnOutsideEsc: false,
|
|
152
152
|
hidesOnOutsideClick: false,
|
|
153
153
|
isTooltip: false,
|
|
154
|
+
isAlertDialog: false,
|
|
154
155
|
invokerRelation: 'description',
|
|
155
156
|
visibilityTriggerFunction: undefined,
|
|
156
157
|
handlesAccessibility: false,
|
|
@@ -381,6 +382,14 @@ export class OverlayController extends EventTarget {
|
|
|
381
382
|
return /** @type {boolean} */ (this.config?.isTooltip);
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
/**
|
|
386
|
+
* The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow
|
|
387
|
+
* to communicate an important message and require a response.
|
|
388
|
+
*/
|
|
389
|
+
get isAlertDialog() {
|
|
390
|
+
return /** @type {boolean} */ (this.config?.isAlertDialog);
|
|
391
|
+
}
|
|
392
|
+
|
|
384
393
|
/**
|
|
385
394
|
* By default, the tooltip content is a 'description' for the invoker (uses aria-describedby).
|
|
386
395
|
* Setting this property to 'label' makes the content function as a label (via aria-labelledby)
|
|
@@ -611,6 +620,10 @@ export class OverlayController extends EventTarget {
|
|
|
611
620
|
});
|
|
612
621
|
// @ts-ignore
|
|
613
622
|
wrappingDialogElement.open = true;
|
|
623
|
+
if (this.isTooltip) {
|
|
624
|
+
// needed to prevent tooltip getting focus in Safari and Firefox
|
|
625
|
+
wrappingDialogElement.setAttribute('tabindex', '-1');
|
|
626
|
+
}
|
|
614
627
|
|
|
615
628
|
this.__wrappingDialogNode.style.display = 'none';
|
|
616
629
|
this.contentWrapperNode.style.zIndex = '1';
|
|
@@ -672,7 +685,10 @@ export class OverlayController extends EventTarget {
|
|
|
672
685
|
if (this.invokerNode && !isModal) {
|
|
673
686
|
this.invokerNode.setAttribute('aria-expanded', `${this.isShown}`);
|
|
674
687
|
}
|
|
675
|
-
if (
|
|
688
|
+
if (this.isAlertDialog) {
|
|
689
|
+
this.contentNode.setAttribute('role', 'alertdialog');
|
|
690
|
+
} else if (!this.contentNode.getAttribute('role')) {
|
|
691
|
+
// N.B. if we did not explicitly set `isTooltip` or `isAlertDialog`, a role potentially already provided by a user (like 'listbox') takes precedence
|
|
676
692
|
this.contentNode.setAttribute('role', 'dialog');
|
|
677
693
|
}
|
|
678
694
|
}
|
|
@@ -1191,9 +1207,8 @@ export class OverlayController extends EventTarget {
|
|
|
1191
1207
|
const hasPressedInside =
|
|
1192
1208
|
event.composedPath().includes(this.contentNode) ||
|
|
1193
1209
|
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
}
|
|
1210
|
+
if (hasPressedInside) return;
|
|
1211
|
+
this.hide();
|
|
1197
1212
|
};
|
|
1198
1213
|
|
|
1199
1214
|
/**
|
|
@@ -1206,7 +1221,7 @@ export class OverlayController extends EventTarget {
|
|
|
1206
1221
|
if (this.invokerNode) {
|
|
1207
1222
|
this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
|
|
1208
1223
|
}
|
|
1209
|
-
} else if (phase === 'hide') {
|
|
1224
|
+
} else if (phase === 'hide' || phase === 'teardown') {
|
|
1210
1225
|
this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
|
|
1211
1226
|
if (this.invokerNode) {
|
|
1212
1227
|
this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
|
|
@@ -1221,7 +1236,7 @@ export class OverlayController extends EventTarget {
|
|
|
1221
1236
|
_handleHidesOnOutsideEsc({ phase }) {
|
|
1222
1237
|
if (phase === 'show') {
|
|
1223
1238
|
document.addEventListener('keyup', this.#outsideEscKeyHandler);
|
|
1224
|
-
} else if (phase === 'hide') {
|
|
1239
|
+
} else if (phase === 'hide' || phase === 'teardown') {
|
|
1225
1240
|
document.removeEventListener('keyup', this.#outsideEscKeyHandler);
|
|
1226
1241
|
}
|
|
1227
1242
|
}
|
|
@@ -24,6 +24,8 @@ export const OverlayMixinImplementation = superclass =>
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
#hasSetup = false;
|
|
28
|
+
|
|
27
29
|
constructor() {
|
|
28
30
|
super();
|
|
29
31
|
/**
|
|
@@ -173,22 +175,23 @@ export const OverlayMixinImplementation = superclass =>
|
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
*/
|
|
179
|
-
firstUpdated(changedProperties) {
|
|
180
|
-
super.firstUpdated(changedProperties);
|
|
178
|
+
connectedCallback() {
|
|
179
|
+
super.connectedCallback();
|
|
181
180
|
|
|
182
|
-
this.
|
|
181
|
+
this.updateComplete.then(() => {
|
|
182
|
+
if (this.#hasSetup) return;
|
|
183
|
+
this._setupOverlayCtrl();
|
|
184
|
+
this.#hasSetup = true;
|
|
185
|
+
});
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
async disconnectedCallback() {
|
|
186
189
|
super.disconnectedCallback();
|
|
187
190
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
if (await this._isPermanentlyDisconnected()) {
|
|
192
|
+
this._teardownOverlayCtrl();
|
|
193
|
+
this.#hasSetup = false;
|
|
194
|
+
}
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
/**
|
|
@@ -238,6 +241,8 @@ export const OverlayMixinImplementation = superclass =>
|
|
|
238
241
|
|
|
239
242
|
/** @protected */
|
|
240
243
|
_setupOverlayCtrl() {
|
|
244
|
+
if (this._overlayCtrl) return;
|
|
245
|
+
|
|
241
246
|
/** @type {OverlayController} */
|
|
242
247
|
this._overlayCtrl = this._defineOverlay({
|
|
243
248
|
contentNode: this._overlayContentNode,
|
|
@@ -253,11 +258,12 @@ export const OverlayMixinImplementation = superclass =>
|
|
|
253
258
|
|
|
254
259
|
/** @protected */
|
|
255
260
|
_teardownOverlayCtrl() {
|
|
261
|
+
if (!this._overlayCtrl) return;
|
|
262
|
+
|
|
256
263
|
this._teardownOpenCloseListeners();
|
|
257
264
|
this.__teardownSyncFromOverlayController();
|
|
258
265
|
|
|
259
|
-
/** @type {OverlayController} */
|
|
260
|
-
(this._overlayCtrl).teardown();
|
|
266
|
+
/** @type {OverlayController} */ (this._overlayCtrl).teardown();
|
|
261
267
|
}
|
|
262
268
|
|
|
263
269
|
/**
|
|
@@ -396,9 +402,9 @@ export const OverlayMixinImplementation = superclass =>
|
|
|
396
402
|
* Before we decide to teardown, let's wait to see if we were not just moving nodes around.
|
|
397
403
|
* @return {Promise<boolean>}
|
|
398
404
|
*/
|
|
399
|
-
async
|
|
405
|
+
async _isPermanentlyDisconnected() {
|
|
400
406
|
await this.updateComplete;
|
|
401
|
-
return this.isConnected;
|
|
407
|
+
return !this.isConnected;
|
|
402
408
|
}
|
|
403
409
|
};
|
|
404
410
|
export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
|
|
@@ -103,12 +103,12 @@ const withLocalTestConfig = () =>
|
|
|
103
103
|
async function createNestedEscControllers(parentContent) {
|
|
104
104
|
const childContent = /** @type {HTMLDivElement} */ (parentContent.querySelector('div[id]'));
|
|
105
105
|
// Assert valid fixure
|
|
106
|
-
const
|
|
107
|
-
(parentContent.id
|
|
108
|
-
parentContent.id
|
|
109
|
-
(childContent.id
|
|
110
|
-
childContent.id
|
|
111
|
-
if (!
|
|
106
|
+
const isValidFixture =
|
|
107
|
+
(parentContent.id.startsWith('parent-overlay--hidesOnEsc') ||
|
|
108
|
+
parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc')) &&
|
|
109
|
+
(childContent.id.startsWith('child-overlay--hidesOnEsc') ||
|
|
110
|
+
childContent.id.startsWith('child-overlay--hidesOnOutsideEsc'));
|
|
111
|
+
if (!isValidFixture) {
|
|
112
112
|
throw new Error('Provide a valid fixture');
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -117,8 +117,8 @@ async function createNestedEscControllers(parentContent) {
|
|
|
117
117
|
shadowRootParent.appendChild(childContent);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const parentHasOutsideOnEsc = parentContent.id
|
|
121
|
-
const childHasOutsideOnEsc = childContent.id
|
|
120
|
+
const parentHasOutsideOnEsc = parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc');
|
|
121
|
+
const childHasOutsideOnEsc = childContent.id.startsWith('child-overlay--hidesOnOutsideEsc');
|
|
122
122
|
|
|
123
123
|
const parentConfig = parentHasOutsideOnEsc ? { hidesOnOutsideEsc: true } : { hidesOnEsc: true };
|
|
124
124
|
const childConfig = childHasOutsideOnEsc ? { hidesOnOutsideEsc: true } : { hidesOnEsc: true };
|
|
@@ -787,6 +787,9 @@ describe('OverlayController', () => {
|
|
|
787
787
|
|
|
788
788
|
expect(parentOverlay.isShown).to.be.false;
|
|
789
789
|
expect(childOverlay.isShown).to.be.true;
|
|
790
|
+
|
|
791
|
+
await childOverlay.teardown();
|
|
792
|
+
await parentOverlay.teardown();
|
|
790
793
|
});
|
|
791
794
|
|
|
792
795
|
it('on [Escape] press outside overlays: parent stays shown, child hides', async () => {
|
|
@@ -1873,6 +1876,22 @@ describe('OverlayController', () => {
|
|
|
1873
1876
|
it.skip('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => {});
|
|
1874
1877
|
it.skip('disables pointer events and selection on inert elements', async () => {});
|
|
1875
1878
|
|
|
1879
|
+
describe('Alert dialog', () => {
|
|
1880
|
+
it('sets role="alertdialog" when isAlertDialog is set', async () => {
|
|
1881
|
+
const invokerNode = /** @type {HTMLElement} */ (
|
|
1882
|
+
await fixture('<div role="button">invoker</div>')
|
|
1883
|
+
);
|
|
1884
|
+
const ctrl = new OverlayController({
|
|
1885
|
+
...withLocalTestConfig(),
|
|
1886
|
+
handlesAccessibility: true,
|
|
1887
|
+
isAlertDialog: true,
|
|
1888
|
+
invokerNode,
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
expect(ctrl.contentNode?.getAttribute('role')).to.equal('alertdialog');
|
|
1892
|
+
});
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1876
1895
|
describe('Tooltip', () => {
|
|
1877
1896
|
it('adds [aria-describedby] on invoker', async () => {
|
|
1878
1897
|
const invokerNode = /** @type {HTMLElement} */ (
|
|
@@ -1919,6 +1938,19 @@ describe('OverlayController', () => {
|
|
|
1919
1938
|
expect(ctrl.contentNode.getAttribute('role')).to.equal('tooltip');
|
|
1920
1939
|
});
|
|
1921
1940
|
|
|
1941
|
+
it('adds tabindex="-1" to the wrappingDialog element', async () => {
|
|
1942
|
+
const invokerNode = /** @type {HTMLElement} */ (
|
|
1943
|
+
await fixture('<div role="button">invoker</div>')
|
|
1944
|
+
);
|
|
1945
|
+
const ctrl = new OverlayController({
|
|
1946
|
+
...withLocalTestConfig(),
|
|
1947
|
+
handlesAccessibility: true,
|
|
1948
|
+
isTooltip: true,
|
|
1949
|
+
invokerNode,
|
|
1950
|
+
});
|
|
1951
|
+
expect(ctrl.__wrappingDialogNode?.getAttribute('tabindex')).to.equal('-1');
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1922
1954
|
describe('Teardown', () => {
|
|
1923
1955
|
it('restores [role] on dialog content', async () => {
|
|
1924
1956
|
const invokerNode = /** @type {HTMLElement} */ (
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
unsafeStatic,
|
|
3
|
+
nextFrame,
|
|
4
|
+
aTimeout,
|
|
5
|
+
defineCE,
|
|
6
|
+
fixture,
|
|
7
|
+
expect,
|
|
8
|
+
html,
|
|
9
|
+
} from '@open-wc/testing';
|
|
4
10
|
import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
|
|
5
|
-
|
|
6
|
-
import '@lion/ui/define/lion-dialog.js';
|
|
7
11
|
import { browserDetection } from '@lion/ui/core.js';
|
|
12
|
+
import { cache } from 'lit/directives/cache.js';
|
|
13
|
+
import '@lion/ui/define/lion-dialog.js';
|
|
14
|
+
import { LitElement } from 'lit';
|
|
15
|
+
import sinon from 'sinon';
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
|
-
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
|
|
11
18
|
* @typedef {import('../types/OverlayMixinTypes.js').DefineOverlayConfig} DefineOverlayConfig
|
|
12
|
-
* @typedef {import('../types/OverlayMixinTypes.js').OverlayHost} OverlayHost
|
|
13
|
-
* @typedef {import('../types/OverlayMixinTypes.js').OverlayMixin} OverlayMixin
|
|
14
|
-
* @typedef {import('lit').LitElement} LitElement
|
|
15
19
|
* @typedef {LitElement & OverlayHost & {_overlayCtrl:OverlayController}} OverlayEl
|
|
20
|
+
* @typedef {import('../types/OverlayMixinTypes.js').OverlayMixin} OverlayMixin
|
|
21
|
+
* @typedef {import('../types/OverlayMixinTypes.js').OverlayHost} OverlayHost
|
|
22
|
+
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
function getGlobalOverlayCtrls() {
|
|
@@ -331,6 +338,173 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
|
|
331
338
|
}).to.not.throw;
|
|
332
339
|
stub.restore();
|
|
333
340
|
});
|
|
341
|
+
|
|
342
|
+
describe('Teardown', () => {
|
|
343
|
+
it('does not teardown when moving dom nodes', async () => {
|
|
344
|
+
const el = /** @type {OverlayEl} */ (
|
|
345
|
+
await fixture(html`
|
|
346
|
+
<${tag}>
|
|
347
|
+
<div slot="content">content</div>
|
|
348
|
+
<button slot="invoker">invoker button</button>
|
|
349
|
+
</${tag}>
|
|
350
|
+
`)
|
|
351
|
+
);
|
|
352
|
+
// @ts-expect-error [allow-protected] in tests
|
|
353
|
+
const teardownSpy = sinon.spy(el, '_teardownOverlayCtrl');
|
|
354
|
+
// @ts-expect-error [allow-protected] in tests
|
|
355
|
+
const spyDisconnected = sinon.spy(el, '_isPermanentlyDisconnected');
|
|
356
|
+
|
|
357
|
+
// move around in dom
|
|
358
|
+
const newParent = document.createElement('div');
|
|
359
|
+
document.body.appendChild(newParent);
|
|
360
|
+
newParent.appendChild(el);
|
|
361
|
+
expect(spyDisconnected.callCount).to.equal(1);
|
|
362
|
+
|
|
363
|
+
const otherParent = document.createElement('div');
|
|
364
|
+
document.body.appendChild(otherParent);
|
|
365
|
+
otherParent.appendChild(el);
|
|
366
|
+
await aTimeout(0);
|
|
367
|
+
|
|
368
|
+
expect(spyDisconnected.callCount).to.equal(2);
|
|
369
|
+
expect(teardownSpy.callCount).to.equal(0);
|
|
370
|
+
|
|
371
|
+
// simulate a permanent disconnect
|
|
372
|
+
otherParent.removeChild(el);
|
|
373
|
+
await aTimeout(0);
|
|
374
|
+
|
|
375
|
+
expect(teardownSpy.callCount).to.equal(1);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('allows to disconnect and reconnect later', async () => {
|
|
379
|
+
const el = /** @type {OverlayEl} */ (
|
|
380
|
+
await fixture(html`
|
|
381
|
+
<${tag}>
|
|
382
|
+
<div slot="content">content</div>
|
|
383
|
+
<button slot="invoker">invoker button</button>
|
|
384
|
+
</${tag}>
|
|
385
|
+
`)
|
|
386
|
+
);
|
|
387
|
+
// @ts-expect-error [allow-protected] in tests
|
|
388
|
+
const teardownSpy = sinon.spy(el, '_teardownOverlayCtrl');
|
|
389
|
+
// @ts-expect-error [allow-protected] in tests
|
|
390
|
+
const spyDisconnected = sinon.spy(el, '_isPermanentlyDisconnected');
|
|
391
|
+
// @ts-expect-error [allow-protected] in tests
|
|
392
|
+
const setupSpy = sinon.spy(el, '_setupOverlayCtrl');
|
|
393
|
+
|
|
394
|
+
// 'permanently' disconnect (although we will reconnect later)
|
|
395
|
+
const offlineParent = document.createElement('div');
|
|
396
|
+
offlineParent.appendChild(el);
|
|
397
|
+
await aTimeout(0);
|
|
398
|
+
|
|
399
|
+
expect(spyDisconnected.callCount).to.equal(1);
|
|
400
|
+
expect(teardownSpy.callCount).to.equal(1);
|
|
401
|
+
|
|
402
|
+
// reconnect
|
|
403
|
+
document.body.appendChild(offlineParent);
|
|
404
|
+
await aTimeout(0);
|
|
405
|
+
|
|
406
|
+
expect(setupSpy.callCount).to.equal(1);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('works with cache directive when reconnected', async () => {
|
|
410
|
+
class CachingContext extends LitElement {
|
|
411
|
+
static properties = {
|
|
412
|
+
switched: { type: Boolean },
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
constructor() {
|
|
416
|
+
super();
|
|
417
|
+
|
|
418
|
+
this.switched = false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
get overlayEl() {
|
|
422
|
+
return this.shadowRoot?.querySelector('#myOverlay');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
render() {
|
|
426
|
+
return html`${cache(
|
|
427
|
+
this.switched
|
|
428
|
+
? html`something else`
|
|
429
|
+
: html`<${tag} id="myOverlay">
|
|
430
|
+
<div slot="content">content b</div>
|
|
431
|
+
<button slot="invoker">invoker button b</button>
|
|
432
|
+
</${tag}>`,
|
|
433
|
+
)}`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const cachingTagName = defineCE(CachingContext);
|
|
438
|
+
const cachingTag = unsafeStatic(cachingTagName);
|
|
439
|
+
|
|
440
|
+
const el = /** @type {CachingContext} */ (
|
|
441
|
+
await fixture(html`
|
|
442
|
+
<${cachingTag}></${cachingTag}>
|
|
443
|
+
`)
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// @ts-expect-error [allow-protected] in tests
|
|
447
|
+
const teardownSpy = sinon.spy(el.overlayEl, '_teardownOverlayCtrl');
|
|
448
|
+
// @ts-expect-error [allow-protected] in tests
|
|
449
|
+
const spyDisconnected = sinon.spy(el.overlayEl, '_isPermanentlyDisconnected');
|
|
450
|
+
// @ts-expect-error [allow-protected] in tests
|
|
451
|
+
const setupSpy = sinon.spy(el.overlayEl, '_setupOverlayCtrl');
|
|
452
|
+
|
|
453
|
+
el.switched = true;
|
|
454
|
+
// render the new content
|
|
455
|
+
await el.updateComplete;
|
|
456
|
+
// wait for the teardown to complete
|
|
457
|
+
await el.updateComplete;
|
|
458
|
+
|
|
459
|
+
expect(spyDisconnected.callCount).to.equal(1);
|
|
460
|
+
expect(teardownSpy.callCount).to.equal(1);
|
|
461
|
+
expect(setupSpy.callCount).to.equal(0);
|
|
462
|
+
|
|
463
|
+
el.switched = false;
|
|
464
|
+
await el.updateComplete;
|
|
465
|
+
|
|
466
|
+
expect(setupSpy.callCount).to.equal(1);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('correctly removes event listeners when disconnected from dom', async () => {
|
|
470
|
+
const el = /** @type {OverlayEl} */ (
|
|
471
|
+
await fixture(html`
|
|
472
|
+
<${tag}>
|
|
473
|
+
<div slot="content">content</div>
|
|
474
|
+
<button slot="invoker">invoker button</button>
|
|
475
|
+
</${tag}>
|
|
476
|
+
`)
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const eventRemoveSpy = sinon.spy(el._overlayCtrl, 'removeEventListener');
|
|
480
|
+
el.parentNode?.removeChild(el);
|
|
481
|
+
await el.updateComplete;
|
|
482
|
+
|
|
483
|
+
expect(eventRemoveSpy.callCount).to.equal(0);
|
|
484
|
+
|
|
485
|
+
await el.updateComplete;
|
|
486
|
+
const hasOneInstanceFor = (
|
|
487
|
+
/** @type {string} */ eventName,
|
|
488
|
+
/** @type {any[][]} */ eventsToSearch,
|
|
489
|
+
/** @type {string} */ methodToRemove,
|
|
490
|
+
) =>
|
|
491
|
+
Boolean(
|
|
492
|
+
eventsToSearch.filter(
|
|
493
|
+
([eventToSearch, methodToSearch]) =>
|
|
494
|
+
eventToSearch === eventName && methodToSearch === methodToRemove,
|
|
495
|
+
).length,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// @ts-expect-error [allow-private] in tests
|
|
499
|
+
expect(hasOneInstanceFor('show', eventRemoveSpy.args, el.__onOverlayCtrlShow)).to.be.true;
|
|
500
|
+
// @ts-expect-error [allow-private] in tests
|
|
501
|
+
expect(hasOneInstanceFor('hide', eventRemoveSpy.args, el.__onOverlayCtrlHide)).to.be.true;
|
|
502
|
+
// @ts-expect-error [allow-private] in tests
|
|
503
|
+
expect(hasOneInstanceFor('before-show', eventRemoveSpy.args, el.__onBeforeShow)).to.be.true;
|
|
504
|
+
// @ts-expect-error [allow-private] in tests
|
|
505
|
+
expect(hasOneInstanceFor('before-hide', eventRemoveSpy.args, el.__onBeforeHide)).to.be.true;
|
|
506
|
+
});
|
|
507
|
+
});
|
|
334
508
|
});
|
|
335
509
|
|
|
336
510
|
describe(`OverlayMixin${suffix} nested`, () => {
|
|
@@ -388,6 +562,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
|
|
|
388
562
|
const moveTarget = /** @type {OverlayEl} */ (await fixture('<div id="target"></div>'));
|
|
389
563
|
moveTarget.appendChild(el);
|
|
390
564
|
await el.updateComplete;
|
|
565
|
+
|
|
391
566
|
expect(getGlobalOverlayCtrls().length).to.equal(1);
|
|
392
567
|
}
|
|
393
568
|
});
|
|
@@ -4,25 +4,34 @@ import { Options as PopperOptions, State } from '@popperjs/core/lib/popper.js';
|
|
|
4
4
|
import { OverlayConfig } from './OverlayConfig.js';
|
|
5
5
|
|
|
6
6
|
export declare class ArrowHost {
|
|
7
|
-
static
|
|
7
|
+
static properties: {
|
|
8
8
|
hasArrow: {
|
|
9
9
|
type: BooleanConstructor;
|
|
10
10
|
reflect: boolean;
|
|
11
11
|
attribute: string;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
+
|
|
14
15
|
hasArrow: boolean;
|
|
16
|
+
|
|
15
17
|
repositionComplete: Promise<void>;
|
|
16
18
|
|
|
17
|
-
static
|
|
19
|
+
static styles: CSSResultArray;
|
|
18
20
|
|
|
19
21
|
render(): TemplateResult;
|
|
22
|
+
|
|
20
23
|
protected _arrowTemplate(): TemplateResult;
|
|
24
|
+
|
|
21
25
|
protected _arrowNodeTemplate(): TemplateResult;
|
|
26
|
+
|
|
22
27
|
protected _defineOverlayConfig(): OverlayConfig;
|
|
28
|
+
|
|
23
29
|
protected _getPopperArrowConfig(popperConfigToExtendFrom: Partial<PopperOptions>): Partial<PopperOptions>;
|
|
30
|
+
|
|
24
31
|
private __setupRepositionCompletePromise(): void;
|
|
32
|
+
|
|
25
33
|
get _arrowNode(): Element | null;
|
|
34
|
+
|
|
26
35
|
private __syncFromPopperState(data: Partial<State>): void;
|
|
27
36
|
}
|
|
28
37
|
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { Options } from '@popperjs/core';
|
|
2
2
|
|
|
3
|
+
export type ViewportPlacement =
|
|
4
|
+
| 'center'
|
|
5
|
+
| 'top-left'
|
|
6
|
+
| 'top'
|
|
7
|
+
| 'top-right'
|
|
8
|
+
| 'right'
|
|
9
|
+
| 'bottom-right'
|
|
10
|
+
| 'bottom'
|
|
11
|
+
| 'bottom-left'
|
|
12
|
+
| 'left';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export interface ViewportConfig {
|
|
16
|
+
placement: ViewportPlacement;
|
|
17
|
+
}
|
|
3
18
|
export interface OverlayConfig {
|
|
4
19
|
// Positioning
|
|
5
20
|
|
|
@@ -26,7 +41,7 @@ export interface OverlayConfig {
|
|
|
26
41
|
contentNode?: HTMLElement;
|
|
27
42
|
/** The wrapper element of contentNode, used to supply inline positioning styles. When a Popper arrow is needed, it acts as parent of the arrow node. Will be automatically created for global and non projected contentNodes. Required when used in shadow dom mode or when Popper arrow is supplied. Essential for allowing webcomponents to style their projected contentNodes */
|
|
28
43
|
contentWrapperNode?: HTMLElement;
|
|
29
|
-
/** The element that is placed
|
|
44
|
+
/** The element that is placed behind the contentNode. When not provided and `hasBackdrop` is true, a backdropNode will be automatically created */
|
|
30
45
|
backdropNode?: HTMLElement;
|
|
31
46
|
/** The element that should be called `.focus()` on after dialog closes */
|
|
32
47
|
elementToFocusAfterHide?: HTMLElement;
|
|
@@ -44,7 +59,7 @@ export interface OverlayConfig {
|
|
|
44
59
|
trapsKeyboardFocus?: boolean;
|
|
45
60
|
/** Hides the overlay when pressing [ esc ] */
|
|
46
61
|
hidesOnEsc?: boolean;
|
|
47
|
-
/** Hides the overlay when clicking next to it,
|
|
62
|
+
/** Hides the overlay when clicking next to it, excluding invoker */
|
|
48
63
|
hidesOnOutsideClick?: boolean;
|
|
49
64
|
/** Hides the overlay when pressing esc, even when contentNode has no focus */
|
|
50
65
|
hidesOnOutsideEsc?: boolean;
|
|
@@ -67,6 +82,8 @@ export interface OverlayConfig {
|
|
|
67
82
|
|
|
68
83
|
/** Has a totally different interaction- and accessibility pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog" element */
|
|
69
84
|
isTooltip?: boolean;
|
|
85
|
+
/** The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow to communicate an important message and require a response. */
|
|
86
|
+
isAlertDialog?: boolean;
|
|
70
87
|
/** By default, the tooltip content is a 'description' for the invoker (uses aria-describedby) Setting this property to 'label' makes the content function as a label (via aria-labelledby) */
|
|
71
88
|
invokerRelation?: 'label' | 'description';
|
|
72
89
|
|
|
@@ -80,17 +97,4 @@ export interface OverlayConfig {
|
|
|
80
97
|
visibilityTriggerFunction?: Function;
|
|
81
98
|
}
|
|
82
99
|
|
|
83
|
-
export type ViewportPlacement =
|
|
84
|
-
| 'center'
|
|
85
|
-
| 'top-left'
|
|
86
|
-
| 'top'
|
|
87
|
-
| 'top-right'
|
|
88
|
-
| 'right'
|
|
89
|
-
| 'bottom-right'
|
|
90
|
-
| 'bottom'
|
|
91
|
-
| 'bottom-left'
|
|
92
|
-
| 'left';
|
|
93
100
|
|
|
94
|
-
export interface ViewportConfig {
|
|
95
|
-
placement: ViewportPlacement;
|
|
96
|
-
}
|
|
@@ -423,6 +423,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|
|
423
423
|
*/
|
|
424
424
|
_teardownOverlayCtrl() {
|
|
425
425
|
super._teardownOverlayCtrl();
|
|
426
|
+
|
|
426
427
|
this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
|
|
427
428
|
this._overlayCtrl.removeEventListener('before-show', this.__overlayBeforeShow);
|
|
428
429
|
this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
|
|
@@ -434,11 +435,12 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|
|
434
435
|
* @protected
|
|
435
436
|
*/
|
|
436
437
|
async _alignInvokerWidth() {
|
|
438
|
+
await this.updateComplete;
|
|
439
|
+
|
|
437
440
|
if (!this._overlayCtrl?.content) {
|
|
438
441
|
return;
|
|
439
442
|
}
|
|
440
443
|
|
|
441
|
-
await this.updateComplete;
|
|
442
444
|
const initContentDisplay = this._overlayCtrl.content.style.display;
|
|
443
445
|
const initContentMinWidth = this._overlayCtrl.contentWrapperNode.style.minWidth;
|
|
444
446
|
const initContentWidth = this._overlayCtrl.contentWrapperNode.style.width;
|
|
@@ -7,6 +7,7 @@ import '@lion/ui/define/lion-select-rich.js';
|
|
|
7
7
|
import '@lion/ui/define/lion-listbox.js';
|
|
8
8
|
import '@lion/ui/define/lion-option.js';
|
|
9
9
|
import { LitElement } from 'lit';
|
|
10
|
+
import sinon from 'sinon';
|
|
10
11
|
import {
|
|
11
12
|
fixture as _fixture,
|
|
12
13
|
unsafeStatic,
|
|
@@ -711,6 +712,49 @@ describe('lion-select-rich', () => {
|
|
|
711
712
|
});
|
|
712
713
|
});
|
|
713
714
|
|
|
715
|
+
describe('Teardown', () => {
|
|
716
|
+
it('correctly removes event listeners when disconnected from dom', async () => {
|
|
717
|
+
const el = await fixture(html`
|
|
718
|
+
<lion-select-rich label="age">
|
|
719
|
+
<lion-option .choiceValue=${10}>Item 1</lion-option>
|
|
720
|
+
<lion-option .choiceValue=${20}>Item 2</lion-option>
|
|
721
|
+
</lion-select-rich>
|
|
722
|
+
`);
|
|
723
|
+
|
|
724
|
+
const { _overlayCtrl } = getSelectRichMembers(el);
|
|
725
|
+
const eventRemoveSpy = sinon.spy(_overlayCtrl, 'removeEventListener');
|
|
726
|
+
|
|
727
|
+
el.parentNode?.removeChild(el);
|
|
728
|
+
|
|
729
|
+
expect(eventRemoveSpy.callCount).to.equal(0);
|
|
730
|
+
|
|
731
|
+
await el.updateComplete;
|
|
732
|
+
expect(eventRemoveSpy.callCount).to.equal(0);
|
|
733
|
+
|
|
734
|
+
await el.updateComplete;
|
|
735
|
+
|
|
736
|
+
const hasOneInstanceFor = (
|
|
737
|
+
/** @type {string} */ eventName,
|
|
738
|
+
/** @type {any[][]} */ eventsToSearch,
|
|
739
|
+
/** @type {string} */ methodToRemove,
|
|
740
|
+
) =>
|
|
741
|
+
Boolean(
|
|
742
|
+
eventsToSearch.filter(
|
|
743
|
+
([eventToSearch, methodToSearch]) =>
|
|
744
|
+
eventToSearch === eventName && methodToSearch === methodToRemove,
|
|
745
|
+
).length,
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
// @ts-expect-error [allow-private] in tests
|
|
749
|
+
expect(hasOneInstanceFor('show', eventRemoveSpy.args, el.__overlayOnShow)).to.be.true;
|
|
750
|
+
// @ts-expect-error [allow-private] in tests
|
|
751
|
+
expect(hasOneInstanceFor('before-show', eventRemoveSpy.args, el.__overlayBeforeShow)).to.be
|
|
752
|
+
.true;
|
|
753
|
+
// @ts-expect-error [allow-private] in tests
|
|
754
|
+
expect(hasOneInstanceFor('hide', eventRemoveSpy.args, el.__overlayOnHide)).to.be.true;
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
714
758
|
describe('Subclassers', () => {
|
|
715
759
|
it('allows to override the type of overlay', async () => {
|
|
716
760
|
const mySelectTagString = defineCE(
|