diff --git a/package.json b/package.json index 2afa9523b..e66d26c3d 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "@open-wc/demoing-storybook": "^0.2.0", "@open-wc/eslint-config": "^1.0.0", "@open-wc/prettier-config": "^0.1.0", - "@open-wc/testing": "^2.3.2", - "@open-wc/testing-karma": "^3.1.22", - "@open-wc/testing-karma-bs": "^1.1.47", + "@open-wc/testing": "^2.3.4", + "@open-wc/testing-karma": "^3.1.33", + "@open-wc/testing-karma-bs": "^1.1.58", "@open-wc/testing-wallaby": "^0.1.12", "@webcomponents/webcomponentsjs": "^2.2.5", "babel-eslint": "^8.2.6", diff --git a/packages/ajax/package.json b/packages/ajax/package.json index 67a662946..7a4a059f3 100644 --- a/packages/ajax/package.json +++ b/packages/ajax/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/button/package.json b/packages/button/package.json index b8f2d7667..cca7ba7c8 100644 --- a/packages/button/package.json +++ b/packages/button/package.json @@ -39,7 +39,7 @@ "@lion/icon": "^0.2.6", "@lion/input": "^0.1.52", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "@polymer/iron-test-helpers": "^3.0.1", "sinon": "^7.2.2" } diff --git a/packages/calendar/package.json b/packages/calendar/package.json index bcecb5486..78927976a 100644 --- a/packages/calendar/package.json +++ b/packages/calendar/package.json @@ -39,7 +39,7 @@ "devDependencies": { "@lion/button": "^0.3.16", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/calendar/src/LionCalendar.js b/packages/calendar/src/LionCalendar.js index 18771d5de..329a8bf49 100644 --- a/packages/calendar/src/LionCalendar.js +++ b/packages/calendar/src/LionCalendar.js @@ -1,5 +1,6 @@ import { html, LitElement } from '@lion/core'; import { localize, getWeekdayNames, getMonthNames, LocalizeMixin } from '@lion/localize'; +import '@lion/core/src/differentKeyEventNamesShimIE.js'; import { createMultipleMonth } from './utils/createMultipleMonth.js'; import { dayTemplate } from './utils/dayTemplate.js'; import { dataTemplate } from './utils/dataTemplate.js'; @@ -7,7 +8,6 @@ import { getFirstDayNextMonth } from './utils/getFirstDayNextMonth.js'; import { getLastDayPreviousMonth } from './utils/getLastDayPreviousMonth.js'; import { isSameDate } from './utils/isSameDate.js'; import { calendarStyle } from './calendarStyle.js'; -import './utils/differentKeyNamesShimIE.js'; import { createDay } from './utils/createDay.js'; import { normalizeDateTime } from './utils/normalizeDateTime.js'; diff --git a/packages/calendar/src/utils/differentKeyNamesShimIE.js b/packages/calendar/src/utils/differentKeyNamesShimIE.js deleted file mode 100644 index 65b6d49d1..000000000 --- a/packages/calendar/src/utils/differentKeyNamesShimIE.js +++ /dev/null @@ -1,33 +0,0 @@ -const event = KeyboardEvent.prototype; -const descriptor = Object.getOwnPropertyDescriptor(event, 'key'); -if (descriptor) { - const keys = { - Win: 'Meta', - Scroll: 'ScrollLock', - Spacebar: ' ', - - Down: 'ArrowDown', - Left: 'ArrowLeft', - Right: 'ArrowRight', - Up: 'ArrowUp', - - Del: 'Delete', - Apps: 'ContextMenu', - Esc: 'Escape', - - Multiply: '*', - Add: '+', - Subtract: '-', - Decimal: '.', - Divide: '/', - }; - Object.defineProperty(event, 'key', { - // eslint-disable-next-line object-shorthand, func-names - get: function() { - const key = descriptor.get.call(this); - - // eslint-disable-next-line no-prototype-builtins - return keys.hasOwnProperty(key) ? keys[key] : key; - }, - }); -} diff --git a/packages/calendar/test/lion-calendar.test.js b/packages/calendar/test/lion-calendar.test.js index 84c0b02f7..cc110906c 100644 --- a/packages/calendar/test/lion-calendar.test.js +++ b/packages/calendar/test/lion-calendar.test.js @@ -1,4 +1,5 @@ import { expect, fixture } from '@open-wc/testing'; +import '@lion/core/test-helpers/keyboardEventShimIE.js'; import sinon from 'sinon'; import { html } from '@lion/core'; @@ -6,7 +7,6 @@ import { localize } from '@lion/localize'; import { localizeTearDown } from '@lion/localize/test-helpers.js'; import { CalendarObject, DayObject } from '../test-helpers.js'; -import './keyboardEventShimIE.js'; import { isSameDate } from '../src/utils/isSameDate.js'; import '../lion-calendar.js'; diff --git a/packages/checkbox-group/package.json b/packages/checkbox-group/package.json index eb21a7070..f41c7d91f 100644 --- a/packages/checkbox-group/package.json +++ b/packages/checkbox-group/package.json @@ -40,7 +40,7 @@ "@lion/form": "^0.1.58", "@lion/localize": "^0.4.15", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/checkbox/package.json b/packages/checkbox/package.json index ae66de169..bee59a281 100644 --- a/packages/checkbox/package.json +++ b/packages/checkbox/package.json @@ -38,6 +38,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/choice-input/package.json b/packages/choice-input/package.json index 1ca00580c..a0cb94731 100644 --- a/packages/choice-input/package.json +++ b/packages/choice-input/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@lion/input": "^0.1.52", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/core/package.json b/packages/core/package.json index 96b8cf5e1..996d6d00c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -26,7 +26,7 @@ "docs", "src", "stories", - "test", + "test-helpers", "translations", "*.js" ], @@ -36,7 +36,7 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/core/src/differentKeyEventNamesShimIE.js b/packages/core/src/differentKeyEventNamesShimIE.js new file mode 100644 index 000000000..ad27b941c --- /dev/null +++ b/packages/core/src/differentKeyEventNamesShimIE.js @@ -0,0 +1,36 @@ +if (typeof window.KeyboardEvent !== 'function') { + // e.g. is IE and needs "polyfill" + const event = KeyboardEvent.prototype; + const descriptor = Object.getOwnPropertyDescriptor(event, 'key'); + if (descriptor) { + const keys = { + Win: 'Meta', + Scroll: 'ScrollLock', + Spacebar: ' ', + + Down: 'ArrowDown', + Left: 'ArrowLeft', + Right: 'ArrowRight', + Up: 'ArrowUp', + + Del: 'Delete', + Apps: 'ContextMenu', + Esc: 'Escape', + + Multiply: '*', + Add: '+', + Subtract: '-', + Decimal: '.', + Divide: '/', + }; + Object.defineProperty(event, 'key', { + // eslint-disable-next-line object-shorthand, func-names + get: function() { + const key = descriptor.get.call(this); + + // eslint-disable-next-line no-prototype-builtins + return keys.hasOwnProperty(key) ? keys[key] : key; + }, + }); + } +} diff --git a/packages/calendar/test/keyboardEventShimIE.js b/packages/core/test-helpers/keyboardEventShimIE.js similarity index 100% rename from packages/calendar/test/keyboardEventShimIE.js rename to packages/core/test-helpers/keyboardEventShimIE.js diff --git a/packages/field/package.json b/packages/field/package.json index b09612086..77c205a99 100644 --- a/packages/field/package.json +++ b/packages/field/package.json @@ -39,7 +39,7 @@ "devDependencies": { "@lion/localize": "^0.4.15", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/fieldset/package.json b/packages/fieldset/package.json index 41ab20dee..f3394f6a3 100644 --- a/packages/fieldset/package.json +++ b/packages/fieldset/package.json @@ -40,7 +40,7 @@ "@lion/input": "^0.1.52", "@lion/localize": "^0.4.15", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/form-system/package.json b/packages/form-system/package.json index aa19bc9fa..36939c3dc 100644 --- a/packages/form-system/package.json +++ b/packages/form-system/package.json @@ -49,6 +49,6 @@ "@lion/textarea": "^0.1.55", "@lion/validate": "^0.2.30", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/form/package.json b/packages/form/package.json index 9fecb9447..d33760637 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -41,7 +41,7 @@ "@lion/textarea": "^0.1.55", "@lion/validate": "^0.2.30", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/icon/package.json b/packages/icon/package.json index 4915a4b09..e5b27539e 100644 --- a/packages/icon/package.json +++ b/packages/icon/package.json @@ -36,6 +36,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/input-amount/package.json b/packages/input-amount/package.json index ca5b750af..c925d22fb 100644 --- a/packages/input-amount/package.json +++ b/packages/input-amount/package.json @@ -40,6 +40,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/input-date/package.json b/packages/input-date/package.json index 1583a4cbd..a940c48b2 100644 --- a/packages/input-date/package.json +++ b/packages/input-date/package.json @@ -40,6 +40,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/input-datepicker/package.json b/packages/input-datepicker/package.json index 57f1935b6..343138d36 100644 --- a/packages/input-datepicker/package.json +++ b/packages/input-datepicker/package.json @@ -47,8 +47,7 @@ "devDependencies": { "@lion/button": "^0.3.16", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", - "@polymer/iron-test-helpers": "^3.0.1", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/input-datepicker/src/LionInputDatepicker.js b/packages/input-datepicker/src/LionInputDatepicker.js index e987f9c86..2ce4d3c0a 100644 --- a/packages/input-datepicker/src/LionInputDatepicker.js +++ b/packages/input-datepicker/src/LionInputDatepicker.js @@ -138,7 +138,7 @@ export class LionInputDatepicker extends LionInputDate { } get _calendarOverlayElement() { - return this._overlayCtrl._container.firstElementChild; + return this._overlayCtrl.contentNode; } get _calendarElement() { @@ -202,7 +202,7 @@ export class LionInputDatepicker extends LionInputDate { _calendarOverlayTemplate() { return html` - + this._overlayCtrl.hide()}> ${this.calendarHeading} ${this._calendarTemplate()} @@ -255,9 +255,9 @@ export class LionInputDatepicker extends LionInputDate { render(this._invokerTemplate(), renderParent); const invokerNode = renderParent.firstElementChild; - // TODO: ModalDialogController should be replaced by a more flexible + // TODO: ModalDialogController could be replaced by a more flexible // overlay, allowing the overlay to switch on smaller screens, for instance from dropdown to - // bottom sheet (working name for this controller: ResponsiveOverlayController) + // bottom sheet via DynamicOverlayController this._overlayCtrl = overlays.add( new ModalDialogController({ contentTemplate: () => this._calendarOverlayTemplate(), diff --git a/packages/input-datepicker/test-helpers/DatepickerInputObject.js b/packages/input-datepicker/test-helpers/DatepickerInputObject.js index 93f8a9d1a..ff8cc4275 100644 --- a/packages/input-datepicker/test-helpers/DatepickerInputObject.js +++ b/packages/input-datepicker/test-helpers/DatepickerInputObject.js @@ -40,7 +40,7 @@ export class DatepickerInputObject { } get overlayEl() { - return this.el._overlayCtrl._container && this.el._overlayCtrl._container.firstElementChild; + return this.el._overlayCtrl.contentNode; } get overlayHeadingEl() { diff --git a/packages/input-datepicker/test/lion-input-datepicker.test.js b/packages/input-datepicker/test/lion-input-datepicker.test.js index 646a0eb2b..440a5593f 100644 --- a/packages/input-datepicker/test/lion-input-datepicker.test.js +++ b/packages/input-datepicker/test/lion-input-datepicker.test.js @@ -8,8 +8,6 @@ import { minMaxDateValidator, isDateDisabledValidator, } from '@lion/validate'; -import { keyCodes } from '@lion/overlays/src/utils/key-codes.js'; -import { keyUpOn } from '@polymer/iron-test-helpers/mock-interactions.js'; import { LionCalendar } from '@lion/calendar'; import { isSameDate } from '@lion/calendar/src/utils/isSameDate.js'; import { DatepickerInputObject } from '../test-helpers.js'; @@ -92,9 +90,22 @@ describe('', () => { const elObj = new DatepickerInputObject(el); await elObj.openCalendar(); expect(elObj.overlayController.isShown).to.equal(true); - // Mimic user input: should fire the 'selected-date-changed' event - // Make sure focus is inside the calendar/overlay - keyUpOn(elObj.calendarEl, keyCodes.escape); + + elObj.overlayController.contentNode.dispatchEvent( + new KeyboardEvent('keyup', { key: 'Escape' }), + ); + expect(elObj.overlayController.isShown).to.equal(false); + }); + + it('closes the calendar via close button', async () => { + const el = await fixture(html` + + `); + const elObj = new DatepickerInputObject(el); + await elObj.openCalendar(); + expect(elObj.overlayController.isShown).to.equal(true); + + elObj.overlayCloseButtonEl.click(); expect(elObj.overlayController.isShown).to.equal(false); }); diff --git a/packages/input-email/package.json b/packages/input-email/package.json index 07f1127c8..ec4561c3d 100644 --- a/packages/input-email/package.json +++ b/packages/input-email/package.json @@ -40,6 +40,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/input-iban/package.json b/packages/input-iban/package.json index 193d00966..6c2d1bdd5 100644 --- a/packages/input-iban/package.json +++ b/packages/input-iban/package.json @@ -41,6 +41,6 @@ "devDependencies": { "@lion/validate": "^0.2.30", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/input/package.json b/packages/input/package.json index dc2f128b2..13a32760a 100644 --- a/packages/input/package.json +++ b/packages/input/package.json @@ -39,6 +39,6 @@ "@lion/localize": "^0.4.15", "@lion/validate": "^0.2.30", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/localize/package.json b/packages/localize/package.json index d6cfb91ff..1006c90f0 100644 --- a/packages/localize/package.json +++ b/packages/localize/package.json @@ -39,7 +39,7 @@ "devDependencies": { "@bundled-es-modules/fetch-mock": "^6.5.2", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/option/package.json b/packages/option/package.json index 12061c614..13aaf4353 100644 --- a/packages/option/package.json +++ b/packages/option/package.json @@ -38,7 +38,7 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/overlays/README.md b/packages/overlays/README.md index 4575d3299..dbfedcb7a 100644 --- a/packages/overlays/README.md +++ b/packages/overlays/README.md @@ -8,7 +8,7 @@ Manages their position on the screen relative to other elements, including other ## Features - [**Overlays Manager**](./docs/OverlaysManager.md), a global repository keeping track of all different types of overlays. -- [**Overlays Occurrences**](./docs/OverlayOccurrences.md), outline of all possible occurrences of overlays. Divided into two main types: +- [**Overlays System: Scope**](./docs/OverlaySystemScope.md), outline of all possible occurrences of overlays. Divided into two main types: - [**Global Overlay Controller**](./docs/GlobalOverlayController.md), controller for overlays relative to the viewport. - [**Local Overlay Controller**](./docs/LocalOverlayController.md), controller for overlays positioned next to invokers they are related to. diff --git a/packages/overlays/docs/GlobalOverlayController.md b/packages/overlays/docs/GlobalOverlayController.md index 222d1926f..b71fad00a 100644 --- a/packages/overlays/docs/GlobalOverlayController.md +++ b/packages/overlays/docs/GlobalOverlayController.md @@ -1,7 +1,8 @@ # GlobalOverlayController -This is a base class for different global overlays (e.g. a dialog, see [Overlay Occurrences](./OverlayOccurrences.md) - the ones positioned relatively to the viewport. -You should not use this controller directly unless you want to create a unique type of global overlays which is not supported out of the box. +This is a base class for different global overlays (e.g. a dialog, see [Overlay System: Scope](./OverlaySystemScope.md) - the ones positioned relatively to the viewport). + +You should not use this controller directly unless you want to create a unique type of global overlays which is not supported out of the box. But for implementation details check out [Overlay System: Implementation](./OverlaySystemImplementation.md). All supported types of global overlays are described below. @@ -25,9 +26,17 @@ const myCtrl = overlays.add( ); ``` +### BottomsheetController + +A specific extension of GlobalOverlayController configured to create accessible dialogs at the bottom of the screen. + +```js +import { BottomsheetController } from '@lion/overlays'; +``` + ### ModalDialogController -A specific extension of GlobalOverlayController configured to create accessible modal dialogs. +A specific extension of GlobalOverlayController configured to create accessible modal dialogs placed in the center of the screen. ```js import { ModalDialogController } from '@lion/overlays'; diff --git a/packages/overlays/docs/LocalOverlayController.md b/packages/overlays/docs/LocalOverlayController.md index 24aa6fcf1..5ef81a742 100644 --- a/packages/overlays/docs/LocalOverlayController.md +++ b/packages/overlays/docs/LocalOverlayController.md @@ -1,7 +1,10 @@ # LocalOverlayController -This is a base class for different local overlays (e.g. a [tooltip](../../tooltip/), see [Overlay System Implementation](./OverlaySystemImplementation.md) - the ones positioned next to invokers they are related to. For more information strictly about the positioning of the content element to the reference element (invoker), please refer to the [positioning documentation](./LocalOverlayPositioning.md) -You should not use this controller directly unless you want to create a unique type of local overlays which is not supported out of the box. +This is a base class for different local overlays (e.g. a [tooltip](../../tooltip/), see [Overlay System: Scope](./OverlaySystemScope.md) - the ones positioned next to invokers they are related to). + +For more information strictly about the positioning of the content element to the reference element (invoker), please refer to the [positioning documentation](./LocalOverlayPositioning.md). + +You should not use this controller directly unless you want to create a unique type of local overlays which is not supported out of the box. But for implementation details check out [Overlay System: Implementation](./OverlaySystemImplementation.md). All supported types of local overlays are described below. diff --git a/packages/overlays/docs/OverlaySystemImplementation.md b/packages/overlays/docs/OverlaySystemImplementation.md index c464b12cb..57992a5c7 100644 --- a/packages/overlays/docs/OverlaySystemImplementation.md +++ b/packages/overlays/docs/OverlaySystemImplementation.md @@ -1,126 +1,98 @@ # Overlay System: Implementation -This document provides an outline of all possible occurrences of overlays found in applications in -general and thus provided by Lion. -For all concepts referred to in this document, please read [Overlay System Scope](./OverlaySystemScope.md). +This document provides an outline of all possible occurrences of overlays found in applications in general and thus provided by Lion. For all concepts referred to in this document, please read [Overlay System Scope](./OverlaySystemScope.md). + +## Base controller + +The BaseController handles the basics of all controllers, and has the following public functions: + +- **show()**, to show the overlay. +- **hide()**, to hide the overlay. +- **toggle()**, to toggle between show and hide. + +All overlays exists of an invoker and a content + +- **invoker**, the element that can trigger showing (and hiding) the overlay. + - invokerNode +- **content**, the toggleable overlays content + - contentTemplate, in most cases the content will be placed inside a template as one of the controller configuration options. + - contentNode, a node can also be used as the content for local overlays (see next section), such as is done in the [popup](../../popup/). ## Local and global overlay controllers Currently, we have a global and a local overlay controller, as two separate entities. Based on provided config, they handle all positioning logic, accessibility and interaction patterns. -All of their configuration options will be described below as part of the _Responsive overlay_ section. -### Connection points and placement contexts +- [GlobalOverlayController](./GlobalOverlayController.md), the ones positioned relatively to the viewport. +- [LocalOverlayController](./LocalOverlayController.md), the ones positioned next to invokers they are related to. -It's currently not clear where the border between global and local overlays lie. They seem to be -separated based on their 'dom connection point' (body vs 'page cursor'(usually invoker sibling)). -However, there is no required relationship here: we can create a modal dialog from -local context('page cursor') as well. +All of their configuration options will be described below as part of the _Configuration options_ section. -Only, we would have a few concerns when creating global overlays from a local connection point: - -- Accessibility will be harder to implement. When wai-aria 1.0 needs to be supported, all siblings - need to have aria-hidden="true" and all parents role="presentation". Not always straightforward - in shadow dom. If we only need to support wai-aria 1.1, we could use aria-modal="true" on the - element with role="dialog". (we basically need to test our supported browsers and screen readers - for compatibility with aria-modal). -- Stacking context need to be managed: the whole 'z-index chain' should win (it's a battle between - parents in the hierarchy). This would require some complex code to cover all edge cases. -- Side effects of parents adding transforms or clipping become a risk. This is hard to detect and - 'counter'. - -When the dom connection point is 'body', content projection will not work, but a template that -can be rendered without being dependent on its context will be required. - -There usually also is a correllation with their relative positioning context: invoker/other -relative element for local(tooltip/popover/dropdown) vs. 'window/viewport level' for global -(dialog/toast/sheet). - -For responsive overlays (see below for an elaborate explanation), we need to switch from global to -local. When we switch the dom connection point, (think of rotating a mobile or tablet), we -will loose the current focus, which can be an a11y concern. This can eventually be 'catched' by -syncing the activeElement (we would only loose the screenreader active element (for instance, -focused cell in table mode)) - -For maximum flexibility, it should be up to the developer to decide how overlays should be rendered, -per instance of an overlay. - -### Responsive overlay +### DynamicOverlayController Based on screen size, we might want to switch the appearance of an overlay. For instance: an application menu can be displayed as a dropdown on desktop, but as a bottom sheet on mobile. + Similarly, a dialog can be displayed as a popover on desktop, but as a (global) dialog on mobile. -To implement such a flexible overlay, we need an 'umbrella' layer that allows for switching between -different configuration options, also between the connection point in dom (global and local). +The DynamicOverlayController is a flexible overlay that can switch between different controllers, also between the connection point in dom (global and local). The switch is only done when the overlay is closed, so the focus isn't lost while switching from one overlay to another. -Luckily, interfaces of Global and OverlayControllers are very similar. -Therefore we can make a wrapping ResponsiveOverlayController. - -### Configuration options for local and global overlays +### Configuration options In total, we should end up with configuration options as depicted below, for all possible overlays. All boolean flags default to 'false'. -Some options are mutually exclusive, in which case their dependent options and requirement will be -mentioned. -Note: a more generic and precise term for all mentionings of `invoker` below would actually be -`relative positioning element`. +Some options are mutually exclusive, in which case their dependent options and requirement will be mentioned. + +> Note: a more generic and precise term for all mentionings of `invoker` below would actually be `relative positioning element`. + +#### Shared configuration options ```text -- {Element} elementToFocusAfterHide - the element that should be called `.focus()` on after dialog closes -- {Boolean} hasBackdrop - whether it should have a backdrop (currently exclusive to globalOverlayController) -- {Boolean} isBlocking - hides other overlays when mutiple are opened (currently exclusive to globalOverlayController) -- {Boolean} preventsScroll - prevents scrolling body content when overlay opened (currently exclusive to globalOverlayController) -- {Boolean} trapsKeyboardFocus - rotates tab, implicitly set when 'isModal' -- {Boolean} hidesOnEsc - hides the overlay when pressing [esc] -- {Boolean} hidesOnOutsideClick - hides the overlay when clicking next to it, exluding invoker. (currently exclusive to localOverlayController) -- {String} cssPosition - 'absolute' or 'fixed'. TODO: choose name that cannot be mistaken for placement like cssPosition or positioningTechnique: https://github.com/ing-bank/lion/pull/61 -- {TemplateResult} contentTemplate -- {TemplateResult} invokerTemplate (currently exclusive to LocalOverlayController) -- {Element} invokerNode (currently exclusive to LocalOverlayController) -- {Element} contentNode (currently exclusive to LocalOverlayController) +- {Boolean} trapsKeyboardFocus - rotates tab, implicitly set when 'isModal'. +- {Boolean} hidesOnEsc - hides the overlay when pressing [esc]. ``` -These options are suggested to be added to the current ones: +#### Global specific configuration options ```text -- {Boolean} isModal - sets aria-modal and/or aria-hidden="true" on siblings -- {Boolean} isGlobal - determines the connection point in DOM (body vs handled by user) TODO: rename to renderToBody? -- {Boolean} isTooltip - has a totally different interaction - and accessibility pattern from all -other overlays, so needed for internals. +- {Element} elementToFocusAfterHide - the element that should be called `.focus()` on after dialog closes. +- {Boolean} hasBackdrop - whether it should have a backdrop. +- {Boolean} isBlocking - hides other overlays when multiple are opened. +- {Boolean} preventsScroll - prevents scrolling body content when overlay opened. +- {Object} viewportConfig + - {String} placement: 'top-left' | 'top' | 'top-right' | 'right' | 'bottom-left' |'bottom' |'bottom-right' |'left' | 'center' +``` + +#### Local specific configuration options + +```text +- {Boolean} hidesOnOutsideClick - hides the overlay when clicking next to it, excluding invoker. +- {String} cssPosition - 'absolute' or 'fixed'. TODO: choose name that cannot be mistaken for placement like cssPosition or positioningTechnique: . +- For positioning checkout [localOverlayPositioning](./localOverlayPositioning.md). +``` + +#### Suggested additions + +```text +- {Boolean} isModal - sets [aria-modal] and/or [aria-hidden="true"] on siblings +- {Boolean} isTooltip - has a totally different interaction - and accessibility pattern from all other overlays, so needed for internals. - {Boolean} handlesUserInteraction - sets toggle on click, or hover when `isTooltip` - {Boolean} handlesAccessibility - - For non `isTooltip`: - - sets aria-expanded="true/false" and aria-haspopup="true" on invokerNode - - sets aria-controls on invokerNode + - sets [aria-expanded="true/false"] and [aria-haspopup="true"] on invokerNode + - sets [aria-controls] on invokerNode - returns focus to invokerNode on hide - sets focus to overlay content(?) - For `isTooltip`: - - sets role="tooltip" and aria-labelledby/aria-describedby on the content -- {Object} popperConfig - - {String} placement - vertical/horizontal position to be supplied to `managePosition`. See https://github.com/ing-bank/lion/pull/61 for current api. Consists of a primary part (where the overlay is located relative from invoker) and secondary alignnment part (how the overlay 'snaps' to the perpendicular boundary of the invoker), separated via '-'. - - primary : 'bottom' | 'top' | 'left' | 'right' | 'over' (this means the overlay will be positioned on top of the invoker. Think for instance of a select dropdown that opens a selected option on top of the invoker (default behavior of ` -
Contenteditable
+
Contenteditable
@@ -271,42 +316,6 @@ storiesOf('Global Overlay System|Global Overlay', module) `; }) - .add('Toast', () => { - let counter = 0; - - function openInfo() { - const toastCtrl = overlays.add( - new GlobalOverlayController({ - contentTemplate: data => html` -
- Title ${data.counter} -

Lorem ipsum ${data.counter}

-
- `, - }), - ); - toastCtrl.sync({ - isShown: true, - data: { counter }, - }); - counter += 1; - setTimeout(() => { - toastCtrl.hide(); - counter -= 1; - }, 2000); - } - - return html` - - -

Very naive toast implementation

-

It does not handle adding new while toasts are getting hidden

- `; - }) .add('In web components', () => { class EditUsernameOverlay extends LionLitElement { static get properties() { @@ -397,9 +406,9 @@ storiesOf('Global Overlay System|Global Overlay', module) this._editOverlay = overlays.add( new GlobalOverlayController({ focusElementAfterHide: this.shadowRoot.querySelector('button'), - contentTemplate: data => html` + contentTemplate: ({ username = 'standard' } = {}) => html` diff --git a/packages/overlays/stories/index.stories.js b/packages/overlays/stories/index.stories.js index 69897cec5..a92071c3a 100644 --- a/packages/overlays/stories/index.stories.js +++ b/packages/overlays/stories/index.stories.js @@ -1,4 +1,6 @@ import './global-overlay.stories.js'; import './modal-dialog.stories.js'; +import './bottomsheet.stories.js'; import './local-overlay.stories.js'; import './local-overlay-placement.stories.js'; +import './dynamic-overlay.stories.js'; diff --git a/packages/overlays/stories/local-overlay-placement.stories.js b/packages/overlays/stories/local-overlay-placement.stories.js index 19cce5ad9..50e119e3b 100644 --- a/packages/overlays/stories/local-overlay-placement.stories.js +++ b/packages/overlays/stories/local-overlay-placement.stories.js @@ -46,17 +46,18 @@ const popupPlacementDemoStyle = css` storiesOf('Local Overlay System|Local Overlay Placement', module) .addParameters({ options: { selectedPanel: 'storybook/actions/actions-panel' } }) .add('Preferred placement overlay absolute', () => { - const popupController = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'UK'; + invokerNode.addEventListener('click', () => popup.toggle()); + + popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, - contentTemplate: () => - html` -
United Kingdom
- `, - invokerTemplate: () => - html` - - `, + contentTemplate: () => html` +
United Kingdom
+ `, + invokerNode, }), ); @@ -64,14 +65,19 @@ storiesOf('Local Overlay System|Local Overlay Placement', module) - +
- ${popupController.invoker} ${popupController.content} + ${invokerNode} ${popup.content}
`; }) .add('Override the popper config', () => { - const popupController = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'UK'; + invokerNode.addEventListener('click', () => popup.toggle()); + + popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, popperConfig: { @@ -100,10 +106,7 @@ storiesOf('Local Overlay System|Local Overlay Placement', module) html`
United Kingdom
`, - invokerTemplate: () => - html` - - `, + invokerNode, }), ); @@ -115,19 +118,22 @@ storiesOf('Local Overlay System|Local Overlay Placement', module) The API is aligned with Popper.js, visit their documentation for more information: Popper.js Docs - +
- ${popupController.invoker} ${popupController.content} + ${invokerNode} ${popup.content}
`; }); /* TODO: Add this when we have a feature in place that adds scrollbars / overflow when no space is available */ /* .add('Space not available', () => { - const popupController = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'UK'; + invokerNode.addEventListener('click', () => popup.toggle()); + let popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, - contentTemplate: () => - html` + contentTemplate: () => html`
Toggle the placement of this overlay with the buttons. Since there is not enough space available on the vertical center or the top for this popup, the popup will get @@ -135,10 +141,7 @@ storiesOf('Local Overlay System|Local Overlay Placement', module) increase/decrease space see the behavior of this.
`, - invokerTemplate: () => - html` - - `, + invokerNode, }), ); @@ -147,11 +150,11 @@ storiesOf('Local Overlay System|Local Overlay Placement', module) ${popupPlacementDemoStyle}
- - + +
- ${popupController.invoker} ${popupController.content} + ${invokerNode} ${popup.content}
`; }); */ diff --git a/packages/overlays/stories/local-overlay.stories.js b/packages/overlays/stories/local-overlay.stories.js index e1ce170cc..a517cd0b7 100644 --- a/packages/overlays/stories/local-overlay.stories.js +++ b/packages/overlays/stories/local-overlay.stories.js @@ -26,18 +26,19 @@ const popupDemoStyle = css` storiesOf('Local Overlay System|Local Overlay', module) .add('Basic', () => { - const popup = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'UK'; + invokerNode.addEventListener('click', () => popup.toggle()); + + popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, hidesOnOutsideClick: true, - contentTemplate: () => - html` -
United Kingdom
- `, - invokerTemplate: () => - html` - - `, + contentTemplate: () => html` +
United Kingdom
+ `, + invokerNode, }), ); return html` @@ -45,26 +46,27 @@ storiesOf('Local Overlay System|Local Overlay', module) ${popupDemoStyle}
- In the ${popup.invoker}${popup.content} the weather is nice. + In the ${invokerNode}${popup.content} the weather is nice.
`; }) .add('Change preferred position', () => { - const popup = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'UK'; + invokerNode.addEventListener('click', () => popup.toggle()); + + popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, hidesOnOutsideClick: true, popperConfig: { placement: 'top-end', }, - contentTemplate: () => - html` -
United Kingdom
- `, - invokerTemplate: () => - html` - - `, + contentTemplate: () => html` +
United Kingdom
+ `, + invokerNode, }), ); return html` @@ -72,12 +74,17 @@ storiesOf('Local Overlay System|Local Overlay', module) ${popupDemoStyle}
- In the ${popup.invoker}${popup.content} the weather is nice. + In the ${invokerNode}${popup.content} the weather is nice.
`; }) .add('Single placement parameter', () => { - const popup = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'Click me'; + invokerNode.addEventListener('click', () => popup.toggle()); + + popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, hidesOnOutsideClick: true, @@ -89,10 +96,7 @@ storiesOf('Local Overlay System|Local Overlay', module) Supplying placement with a single parameter will assume 'center' for the other. `, - invokerTemplate: () => - html` - - `, + invokerNode, }), ); return html` @@ -100,12 +104,18 @@ storiesOf('Local Overlay System|Local Overlay', module) ${popupDemoStyle}
- ${popup.invoker}${popup.content} + ${invokerNode}${popup.content}
`; }) .add('On hover', () => { - const popup = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'UK'; + invokerNode.addEventListener('mouseenter', () => popup.show()); + invokerNode.addEventListener('mouseleave', () => popup.hide()); + + popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, hidesOnOutsideClick: true, @@ -116,9 +126,7 @@ storiesOf('Local Overlay System|Local Overlay', module) html`
United Kingdom
`, - invokerTemplate: () => html` - - `, + invokerNode, }), ); return html` @@ -126,26 +134,24 @@ storiesOf('Local Overlay System|Local Overlay', module) ${popupDemoStyle}
- In the beautiful ${popup.invoker}${popup.content} the weather is nice. + In the beautiful ${invokerNode}${popup.content} the weather is nice.
`; }) .add('On an input', () => { - const popup = overlays.add( + let popup; + const invokerNode = document.createElement('input'); + invokerNode.id = 'input'; + invokerNode.type = 'text'; + invokerNode.addEventListener('focusin', () => popup.show()); + invokerNode.addEventListener('focusout', () => popup.hide()); + + popup = overlays.add( new LocalOverlayController({ - contentTemplate: () => - html` -
United Kingdom
- `, - invokerTemplate: () => - html` - popup.show()} - @blur=${() => popup.hide()} - /> - `, + contentTemplate: () => html` +
United Kingdom
+ `, + invokerNode, }), ); return html` @@ -154,12 +160,17 @@ storiesOf('Local Overlay System|Local Overlay', module)
- ${popup.invoker}${popup.content} + ${invokerNode}${popup.content}
`; }) .add('trapsKeyboardFocus', () => { - const popup = overlays.add( + let popup; + const invokerNode = document.createElement('button'); + invokerNode.innerHTML = 'Click me'; + invokerNode.addEventListener('click', () => popup.toggle()); + + popup = overlays.add( new LocalOverlayController({ hidesOnEsc: true, hidesOnOutsideClick: true, @@ -177,10 +188,7 @@ storiesOf('Local Overlay System|Local Overlay', module) `, - invokerTemplate: () => - html` - - `, + invokerNode, }), ); return html` @@ -188,7 +196,7 @@ storiesOf('Local Overlay System|Local Overlay', module) ${popupDemoStyle}
- ${popup.invoker}${popup.content} + ${invokerNode}${popup.content}
`; }) @@ -222,7 +230,7 @@ storiesOf('Local Overlay System|Local Overlay', module) ${popupDemoStyle}
- ${popup.invoker}${popup.content} + ${invokerNode}${popup.content}
`; }); diff --git a/packages/overlays/stories/modal-dialog.stories.js b/packages/overlays/stories/modal-dialog.stories.js index 7edcb2032..0bab1651a 100644 --- a/packages/overlays/stories/modal-dialog.stories.js +++ b/packages/overlays/stories/modal-dialog.stories.js @@ -6,26 +6,37 @@ import { overlays, ModalDialogController } from '../index.js'; const modalDialogDemoStyle = css` .demo-overlay { background-color: white; - position: fixed; - top: 20px; - left: 20px; width: 200px; - border: 1px solid blue; - } - - .demo-overlay--2 { - left: 240px; + border: 1px solid lightgrey; } `; storiesOf('Global Overlay System|Modal Dialog', module) .add('Default', () => { + const nestedDialogCtrl = overlays.add( + new ModalDialogController({ + contentTemplate: () => html` +
+

Nested modal dialog

+ +
+ `, + }), + ); + const dialogCtrl = overlays.add( new ModalDialogController({ contentTemplate: () => html`

Modal dialog

+
`, }), diff --git a/packages/overlays/test-suites/BaseOverlayController.suite.js b/packages/overlays/test-suites/BaseOverlayController.suite.js new file mode 100644 index 000000000..ff0217b7b --- /dev/null +++ b/packages/overlays/test-suites/BaseOverlayController.suite.js @@ -0,0 +1,368 @@ +import { expect, html, fixture } from '@open-wc/testing'; +import '@lion/core/test-helpers/keyboardEventShimIE.js'; +import sinon from 'sinon'; +import { keyCodes } from '../src/utils/key-codes.js'; +import { simulateTab } from '../src/utils/simulate-tab.js'; + +export const runBaseOverlaySuite = createCtrlFn => { + describe('shown', () => { + it('has .isShown which defaults to false', () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + expect(ctrl.isShown).to.be.false; + }); + + it('has async show() which shows the overlay', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + await ctrl.show(); + expect(ctrl.isShown).to.be.true; + expect(ctrl.show()).to.be.instanceOf(Promise); + }); + + it('has async hide() which hides the overlay', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + await ctrl.hide(); + expect(ctrl.isShown).to.be.false; + expect(ctrl.hide()).to.be.instanceOf(Promise); + }); + + it('fires "show" event once overlay becomes shown', async () => { + const showSpy = sinon.spy(); + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + ctrl.addEventListener('show', showSpy); + await ctrl.show(); + expect(showSpy.callCount).to.equal(1); + await ctrl.show(); + expect(showSpy.callCount).to.equal(1); + }); + + it('fires "hide" event once overlay becomes hidden', async () => { + const hideSpy = sinon.spy(); + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + ctrl.addEventListener('hide', hideSpy); + await ctrl.show(); + await ctrl.hide(); + expect(hideSpy.callCount).to.equal(1); + await ctrl.hide(); + expect(hideSpy.callCount).to.equal(1); + }); + }); + + describe('.contentTemplate', () => { + it('has .content as a wrapper for a render target', () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + expect(ctrl.content.tagName).to.equal('DIV'); + }); + + it('throws if trying to assign a non function value to .contentTemplate', () => { + expect(() => + createCtrlFn({ + contentTemplate: 'foo', + }), + ).to.throw('.contentTemplate needs to be a function'); + }); + + it('has .contentTemplate to render into .content', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + await ctrl.show(); + expect(ctrl.content).to.have.trimmed.text('my content'); + }); + + it('throws if .contentTemplate does not return a single child node', async () => { + expect(() => { + createCtrlFn({ + contentTemplate: () => html``, + }); + }).to.throw('The .contentTemplate needs to always return exactly one child node'); + + expect(() => { + createCtrlFn({ + contentTemplate: () => html` +

one

+

two

+ `, + }); + }).to.throw('The .contentTemplate needs to always return exactly one child node'); + }); + + it('allows to change the .contentTemplate', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + await ctrl.show(); + expect(ctrl.contentNode).to.have.trimmed.text('my content'); + + ctrl.contentTemplate = () => html` +
+

new content

+

my adjusted content

+
+ `; + expect(ctrl.contentNode).lightDom.to.equal(` +

new content

+

my adjusted content

+ `); + }); + + it('has .contentData which triggers a updates of the overlay content', async () => { + const ctrl = createCtrlFn({ + contentTemplate: ({ username = 'default user' } = {}) => html` +

my content - ${username}

+ `, + }); + await ctrl.show(); + expect(ctrl.content).to.have.trimmed.text('my content - default user'); + + ctrl.contentData = { username: 'foo user' }; + expect(ctrl.content).to.have.trimmed.text('my content - foo user'); + }); + }); + + describe('.contentNode', () => { + it('accepts an .contentNode to directly set content', async () => { + const ctrl = createCtrlFn({ + contentNode: await fixture('

direct node

'), + }); + expect(ctrl.content).to.have.trimmed.text('direct node'); + }); + + it('throws if .contentData gets used without a .contentTemplate', async () => { + const ctrl = createCtrlFn({ + contentNode: await fixture('

direct node

'), + }); + expect(() => { + ctrl.contentData = {}; + }).to.throw('.contentData can only be used if there is a .contentTemplate'); + }); + }); + + describe('_showHideMode="dom" (auto selected with .contentTemplate)', () => { + it('removes dom content on hide', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + + await ctrl.show(); + expect(ctrl.content).to.have.trimmed.text('my content'); + await ctrl.hide(); + expect(ctrl.content).to.be.empty; + }); + }); + + describe('_showHideMode="css" (auto selected with .contentNode)', () => { + it('hides .contentNode via css on hide', async () => { + const ctrl = createCtrlFn({ + contentNode: await fixture('

direct node

'), + }); + + await ctrl.show(); + expect(ctrl.contentNode).to.be.displayed; + + await ctrl.hide(); + expect(ctrl.contentNode).not.to.be.displayed; + + await ctrl.show(); + expect(ctrl.contentNode).to.be.displayed; + }); + + // do we even want to support contentTemplate? + it.skip('hides .contentNode from a .contentTemplate via css on hide', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

direct node

+ `, + }); + ctrl.__showHideMode = 'css'; + + await ctrl.show(); + expect(ctrl.contentNode).to.be.displayed; + + await ctrl.hide(); + expect(ctrl.contentNode).not.to.be.displayed; + + await ctrl.show(); + expect(ctrl.contentNode).to.be.displayed; + }); + + it.skip('does not put a style display on .content when using a .contentTemplate', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

direct node

+ `, + }); + ctrl.__showHideMode = 'css'; + + await ctrl.show(); + expect(ctrl.content.style.display).to.be.empty; + + await ctrl.hide(); + expect(ctrl.content.style.display).to.be.empty; + + await ctrl.show(); + expect(ctrl.content.style.display).to.be.empty; + }); + }); + + describe('setup', () => { + it('throws if .contentTemplate and .contentNode get passed on', async () => { + const node = await fixture('

direct node

'); + expect(() => { + createCtrlFn({ + contentTemplate: () => '', + contentNode: node, + }); + }).to.throw('You can only provide a .contentTemplate or a .contentNode but not both'); + }); + + it('throws if neither .contentTemplate or .contentNode get passed on', async () => { + expect(() => { + createCtrlFn(); + }).to.throw('You need to provide a .contentTemplate or a .contentNode'); + }); + }); + + describe('invoker', () => { + // same as content just with invoker + }); + + describe('switching', () => { + it('has a switchOut/In function', () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

my content

+ `, + }); + expect(ctrl.switchIn).to.be.a('function'); + expect(ctrl.switchOut).to.be.a('function'); + }); + }); + + describe('trapsKeyboardFocus (for a11y)', () => { + it('focuses the overlay on show', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

Content

+ `, + }); + // add element to dom to allow focus + await fixture(html` + ${ctrl.content} + `); + await ctrl.show(); + ctrl.enableTrapsKeyboardFocus(); + expect(ctrl.contentNode).to.equal(document.activeElement); + }); + + it('keeps focus within the overlay e.g. you can not tab out by accident', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +
+ `, + }); + // add element to dom to allow focus + await fixture(html` + ${ctrl.content} + `); + await ctrl.show(); + ctrl.enableTrapsKeyboardFocus(); + + const elOutside = await fixture(html` + + `); + const input1 = ctrl.contentNode.querySelectorAll('input')[0]; + const input2 = ctrl.contentNode.querySelectorAll('input')[1]; + + input2.focus(); + // this mimics a tab within the contain-focus system used + const event = new CustomEvent('keydown', { detail: 0, bubbles: true }); + event.keyCode = keyCodes.tab; + window.dispatchEvent(event); + + expect(elOutside).to.not.equal(document.activeElement); + expect(input1).to.equal(document.activeElement); + }); + + it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +
+ `, + }); + // add element to dom to allow focus + await fixture(html` + ${ctrl.content} + `); + await ctrl.show(); + ctrl.enableTrapsKeyboardFocus(); + + const elOutside = await fixture(html` + + `); + const input = ctrl.contentNode.querySelector('input'); + + input.focus(); + simulateTab(); + + expect(elOutside).to.equal(document.activeElement); + }); + }); + + describe('hidesOnEsc', () => { + it('hides when [escape] is pressed', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

Content

+ `, + }); + await ctrl.show(); + ctrl.enableHidesOnEsc(); + + ctrl.contentNode.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); + expect(ctrl.isShown).to.be.false; + }); + + it('stays shown when [escape] is pressed on outside element', async () => { + const ctrl = createCtrlFn({ + contentTemplate: () => html` +

Content

+ `, + }); + await ctrl.show(); + ctrl.enableHidesOnEsc(); + + document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' })); + expect(ctrl.isShown).to.be.true; + }); + }); +}; diff --git a/packages/overlays/test/BaseOverlayController.test.js b/packages/overlays/test/BaseOverlayController.test.js new file mode 100644 index 000000000..1bcd597de --- /dev/null +++ b/packages/overlays/test/BaseOverlayController.test.js @@ -0,0 +1,6 @@ +import { runBaseOverlaySuite } from '../test-suites/BaseOverlayController.suite.js'; +import { BaseOverlayController } from '../src/BaseOverlayController.js'; + +describe('BaseOverlayController', () => { + runBaseOverlaySuite((...args) => new BaseOverlayController(...args)); +}); diff --git a/packages/overlays/test/BottomsheetController.test.js b/packages/overlays/test/BottomsheetController.test.js new file mode 100644 index 000000000..ad4152f78 --- /dev/null +++ b/packages/overlays/test/BottomsheetController.test.js @@ -0,0 +1,32 @@ +import { expect, html } from '@open-wc/testing'; + +import { GlobalOverlayController } from '../src/GlobalOverlayController.js'; +import { BottomsheetController } from '../src/BottomsheetController.js'; + +describe('BottomsheetController', () => { + let defaultOptions; + + before(() => { + defaultOptions = { + contentTemplate: () => html` +

my content

+ `, + }; + }); + + it('extends GlobalOverlayController', () => { + expect(new BottomsheetController(defaultOptions)).to.be.instanceof(GlobalOverlayController); + }); + + it('has correct defaults', () => { + const controller = new BottomsheetController(defaultOptions); + expect(controller.hasBackdrop).to.equal(true); + expect(controller.isBlocking).to.equal(false); + expect(controller.preventsScroll).to.equal(true); + expect(controller.trapsKeyboardFocus).to.equal(true); + expect(controller.hidesOnEsc).to.equal(true); + expect(controller.overlayContainerPlacementClass).to.equal( + 'global-overlays__overlay-container--bottom', + ); + }); +}); diff --git a/packages/overlays/test/DynamicGlobalLocal.test.js b/packages/overlays/test/DynamicGlobalLocal.test.js new file mode 100644 index 000000000..e1591e2eb --- /dev/null +++ b/packages/overlays/test/DynamicGlobalLocal.test.js @@ -0,0 +1,234 @@ +import { expect, html, fixture } from '@open-wc/testing'; + +import { DynamicOverlayController } from '../src/DynamicOverlayController.js'; +import { GlobalOverlayController } from '../src/GlobalOverlayController.js'; +import { LocalOverlayController } from '../src/LocalOverlayController.js'; +import { overlays } from '../src/overlays.js'; + +function expectGlobalShown(ctrl) { + const allOverlays = Array.from(document.body.querySelectorAll('.global-overlays__overlay')); + expect(allOverlays).to.contain(ctrl.contentNode); + expect(ctrl.contentNode).dom.to.equal('

Content

', { ignoreAttributes: ['class'] }); +} +function expectDomHidden(ctrl) { + const allOverlays = Array.from(document.body.querySelectorAll('.global-overlays__overlay')); + expect(allOverlays).to.not.contain(ctrl.contentNode); +} + +function expectLocalShown(ctrl) { + expect(ctrl.contentNode).dom.to.equal('

Content

', { + ignoreAttributes: ['x-placement', 'style'], + }); + expect(ctrl.contentNode).to.be.displayed; +} +function expectCssHidden(ctrl) { + expect(ctrl.contentNode).dom.to.equal('

Content

', { + ignoreAttributes: ['style', 'x-placement'], + }); + expect(ctrl.contentNode).to.not.be.displayed; +} + +function expectToBeHidden(what) { + if (what._showHideMode === 'css') { + expectCssHidden(what); + } else { + expectDomHidden(what); + } +} + +function expectToBeShown(what) { + if (what instanceof GlobalOverlayController) { + expectGlobalShown(what); + } else { + expectLocalShown(what); + } +} + +async function canSwitchBetween(from, to) { + const ctrl = new DynamicOverlayController(); + ctrl.add(from); + ctrl.add(to); + + // setup: we show/hide to make sure everything is nicely rendered + await from.show(); + await from.hide(); + await to.show(); + await to.hide(); + expect(from.isShown).to.be.false; + expect(to.isShown).to.be.false; + expectToBeHidden(from); + expectToBeHidden(to); + + ctrl.switchTo(to); + await ctrl.show(); + expect(from.isShown).to.be.false; + expect(to.isShown).to.be.true; + expectToBeHidden(from); + expectToBeShown(to); + + await ctrl.hide(); + ctrl.switchTo(from); + await ctrl.show(); + expect(from.isShown).to.be.true; + expect(to.isShown).to.be.false; + expectToBeShown(from); + expectToBeHidden(to); +} + +describe('Dynamic Global and Local Overlay Controller switching', () => { + describe('.contentTemplate switches', () => { + let globalWithTemplate; + let globalWithTemplate1; + let localWithTemplate; + let localWithTemplate1; + + beforeEach(async () => { + const invokerNode = await fixture(''); + globalWithTemplate = overlays.add( + new GlobalOverlayController({ + contentTemplate: () => html` +

Content

+ `, + }), + ); + globalWithTemplate1 = overlays.add( + new GlobalOverlayController({ + contentTemplate: () => html` +

Content

+ `, + }), + ); + localWithTemplate = new LocalOverlayController({ + contentTemplate: () => html` +

Content

+ `, + invokerNode, + }); + localWithTemplate1 = new LocalOverlayController({ + contentTemplate: () => html` +

Content

+ `, + invokerNode, + }); + }); + + afterEach(() => { + overlays.teardown(); + }); + + it(`can switch from localWithTemplate to globalWithTemplate and back`, async () => { + await canSwitchBetween(localWithTemplate, globalWithTemplate); + }); + + it(`can switch from localWithTemplate to localWithTemplate1 and back`, async () => { + await canSwitchBetween(localWithTemplate, localWithTemplate1); + }); + + it(`can switch from globalWithTemplate to localWithTemplate and back`, async () => { + await canSwitchBetween(globalWithTemplate, localWithTemplate); + }); + + it(`can switch from globalWithTemplate to globalWithTemplate1 and back`, async () => { + await canSwitchBetween(globalWithTemplate, globalWithTemplate1); + }); + }); + + // do we want to support this? + describe.skip('.contentNode switches', () => { + let globalWithNodes; + let globalWithNodes1; + let localWithNodes; + let localWithNodes1; + + beforeEach(async () => { + const invokerNode = await fixture(''); + const contentNode = await fixture(`

Content

`); + globalWithNodes = new GlobalOverlayController({ + contentNode, + }); + globalWithNodes1 = new GlobalOverlayController({ + contentNode, + }); + + localWithNodes = new LocalOverlayController({ + contentNode, + invokerNode, + }); + localWithNodes1 = new LocalOverlayController({ + contentNode, + invokerNode, + }); + }); + + afterEach(() => { + overlays.teardown(); + }); + + it(`can switch from localWithNodes to globalWithNodes and back`, async () => { + await canSwitchBetween(localWithNodes, globalWithNodes); + }); + + it(`can switch from localWithNodes to localWithNodes1 and back`, async () => { + await canSwitchBetween(localWithNodes, localWithNodes1); + }); + + it(`can switch from globalWithNodes to localWithNodes and back`, async () => { + await canSwitchBetween(globalWithNodes, localWithNodes); + }); + + it(`can switch from globalWithNodes to globalWithNodes1 and back`, async () => { + await canSwitchBetween(globalWithNodes, globalWithNodes1); + }); + }); + + // do we want to support this? + describe.skip('.contentTemplate/.contentNode switches', () => { + let globalWithTemplate; + let localWithTemplate; + let globalWithNodes; + let localWithNodes; + + beforeEach(async () => { + const invokerNode = await fixture(''); + const contentNode = await fixture(`

Content

`); + globalWithTemplate = new GlobalOverlayController({ + contentTemplate: () => html` +

Content

+ `, + }); + localWithTemplate = new LocalOverlayController({ + contentTemplate: () => html` +

Content

+ `, + invokerNode, + }); + globalWithNodes = new GlobalOverlayController({ + contentNode, + }); + localWithNodes = new LocalOverlayController({ + contentNode, + invokerNode, + }); + }); + + afterEach(() => { + overlays.teardown(); + }); + + it(`can switch from localWithNodes to globalWithTemplate and back`, async () => { + await canSwitchBetween(localWithNodes, globalWithTemplate); + }); + + it(`can switch from localWithTemplate to globalWithNodes and back`, async () => { + await canSwitchBetween(localWithTemplate, globalWithNodes); + }); + + it(`can switch from globalWithTemplate to localWithNodes and back`, async () => { + await canSwitchBetween(globalWithTemplate, localWithNodes); + }); + + it(`can switch from globalWithNodes to localWithTemplate and back`, async () => { + await canSwitchBetween(globalWithNodes, localWithTemplate); + }); + }); +}); diff --git a/packages/overlays/test/DynamicOverlayController.test.js b/packages/overlays/test/DynamicOverlayController.test.js new file mode 100644 index 000000000..07a8daf4a --- /dev/null +++ b/packages/overlays/test/DynamicOverlayController.test.js @@ -0,0 +1,136 @@ +import { expect, html } from '@open-wc/testing'; +import sinon from 'sinon'; + +import { DynamicOverlayController } from '../src/DynamicOverlayController.js'; +import { BaseOverlayController } from '../src/BaseOverlayController.js'; + +describe('DynamicOverlayController', () => { + class FakeLocalCtrl extends BaseOverlayController {} + class FakeGlobalCtrl extends BaseOverlayController {} + + const defaultOptions = { + contentTemplate: () => html` +

my content

+ `, + }; + + it('can add/remove controllers', () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + const local = new FakeLocalCtrl(defaultOptions); + const local2 = new FakeLocalCtrl(defaultOptions); + ctrl.add(global); + ctrl.add(local); + ctrl.add(local2); + + expect(ctrl.list).to.deep.equal([global, local, local2]); + + ctrl.remove(local2); + expect(ctrl.list).to.deep.equal([global, local]); + + ctrl.remove(local); + expect(ctrl.list).to.deep.equal([global]); + }); + + it('throws if you try to add the same controller twice', () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + ctrl.add(global); + expect(() => ctrl.add(global)).to.throw('controller instance is already added'); + }); + + it('will set the first added controller as active', () => { + const ctrl = new DynamicOverlayController(); + expect(ctrl.active).to.be.undefined; + + const global = new FakeGlobalCtrl(defaultOptions); + ctrl.add(global); + + expect(ctrl.active).to.equal(global); + }); + + it('throws if you try to remove a non existing controller', () => { + const ctrl = new DynamicOverlayController(); + const global = new BaseOverlayController(defaultOptions); + expect(() => ctrl.remove(global)).to.throw('could not find controller to remove'); + }); + + it('will throw if you try to remove the active controller', () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + ctrl.add(global); + + expect(() => ctrl.remove(global)).to.throw( + 'You can not remove the active controller. Please switch first to a different controller via ctrl.switchTo()', + ); + }); + + it('can switch the active controller', () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + const local = new FakeLocalCtrl(defaultOptions); + ctrl.add(global); + ctrl.add(local); + + expect(ctrl.active).to.equal(global); + + ctrl.switchTo(local); + expect(ctrl.active).to.equal(local); + + ctrl.switchTo(global); + expect(ctrl.active).to.equal(global); + }); + + it('will call the active controllers show/hide when using .show() / .hide()', async () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + ctrl.add(global); + + const showSpy = sinon.spy(global, 'show'); + const hideSpy = sinon.spy(global, 'hide'); + + await ctrl.show(); + expect(showSpy).to.has.callCount(1); + + await ctrl.hide(); + expect(hideSpy).to.has.callCount(1); + }); + + it('will throw when trying to switch while overlay is shown', async () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + const local = new FakeLocalCtrl(defaultOptions); + ctrl.add(global); + ctrl.add(local); + + await ctrl.show(); + expect(() => { + ctrl.switchTo(local); + }).to.throw('You can not switch overlays while being shown'); + }); + + it('will call switchIn/Out functions of controllers', () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + const local = new FakeLocalCtrl(defaultOptions); + ctrl.add(global); + ctrl.add(local); + + const globalOutSpy = sinon.spy(global, 'switchOut'); + const globalInSpy = sinon.spy(global, 'switchIn'); + const localOutSpy = sinon.spy(local, 'switchOut'); + const localInSpy = sinon.spy(local, 'switchIn'); + + ctrl.switchTo(local); + expect(globalOutSpy).to.have.callCount(1); + expect(localInSpy).to.have.callCount(1); + + ctrl.switchTo(global); + expect(globalInSpy).to.have.callCount(1); + expect(localOutSpy).to.have.callCount(1); + + // sanity check that wrong functions are not called + expect(globalOutSpy).to.have.callCount(1); + expect(localInSpy).to.have.callCount(1); + }); +}); diff --git a/packages/overlays/test/GlobalOverlayController.test.js b/packages/overlays/test/GlobalOverlayController.test.js index 9a9447970..62f2c8010 100644 --- a/packages/overlays/test/GlobalOverlayController.test.js +++ b/packages/overlays/test/GlobalOverlayController.test.js @@ -1,10 +1,8 @@ import { expect, fixture, html } from '@open-wc/testing'; -import { keyUpOn } from '@polymer/iron-test-helpers/mock-interactions.js'; -import { keyCodes } from '../src/utils/key-codes.js'; -import { simulateTab } from '../src/utils/simulate-tab.js'; -import { getDeepActiveElement } from '../src/utils/get-deep-active-element.js'; import { GlobalOverlayController } from '../src/GlobalOverlayController.js'; +import { overlays } from '../src/overlays.js'; +import { runBaseOverlaySuite } from '../test-suites/BaseOverlayController.suite.js'; function getRootNode() { return document.querySelector('.global-overlays'); @@ -51,642 +49,299 @@ function getRenderedOverlay(index) { function cleanup() { document.body.removeAttribute('style'); - if (GlobalOverlayController._rootNode) { - GlobalOverlayController._rootNode.parentElement.removeChild(GlobalOverlayController._rootNode); - GlobalOverlayController._rootNode = undefined; - } + overlays.teardown(); } describe('GlobalOverlayController', () => { afterEach(cleanup); + describe('extends BaseOverlayController', () => { + runBaseOverlaySuite((...args) => overlays.add(new GlobalOverlayController(...args))); + }); + describe('basics', () => { - it('creates a controller with methods: show, hide, sync', () => { - const controller = new GlobalOverlayController(); - expect(controller.show).to.be.a('function'); - expect(controller.hide).to.be.a('function'); - expect(controller.sync).to.be.a('function'); - }); - - it('creates a root node in body when first controller is shown', () => { - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` -

Content

+ it('renders an overlay from the lit-html based contentTemplate when showing', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + contentTemplate: () => html` +

my content

`, - }); - expect(document.body.querySelectorAll('.global-overlays').length).to.equal(0); - controller.show(); - expect(document.body.querySelectorAll('.global-overlays').length).to.equal(1); - expect(document.body.querySelector('.global-overlays')).to.equal( - GlobalOverlayController._rootNode, + }), ); - expect(document.body.querySelector('.global-overlays').parentElement).to.equal(document.body); - expect(GlobalOverlayController._rootNode.children.length).to.equal(1); - }); - - it('renders an overlay from the lit-html based contentTemplate when showing', () => { - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - }); - controller.show(); + await ctrl.show(); expect(getRootNode().children.length).to.equal(1); - expect(getRootNode().children[0].classList.contains('global-overlays__overlay')).to.be.true; - expect(getRootNode().children[0].children.length).to.equal(1); - expect(getRootNode().children[0].children[0].tagName).to.equal('P'); - expect(getRootNode().children[0].children[0].textContent).to.equal('Content'); + expect(getRootNode().children[0]).to.have.trimmed.text('my content'); }); - it('removes the overlay from DOM when hiding', () => { - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` -

Content

+ it('removes the overlay from DOM when hiding', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + viewportConfig: { + placement: 'top-left', + }, + contentTemplate: () => html` +
Content
`, - }); + }), + ); - controller.show(); + await ctrl.show(); expect(getRenderedContainers().length).to.equal(1); - expect(getRenderedOverlay(0).tagName).to.equal('P'); + expect(getRenderedOverlay(0).tagName).to.equal('DIV'); expect(getRenderedOverlay(0).textContent).to.equal('Content'); expect(getTopContainer()).to.equal(getRenderedContainer(0)); - controller.hide(); + await ctrl.hide(); expect(getRenderedContainers().length).to.equal(0); expect(getTopContainer()).to.not.exist; }); - it('exposes isShown state for reading', () => { - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` + it('exposes isShown state for reading', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + contentTemplate: () => html`

Content

`, - }); + }), + ); - expect(controller.isShown).to.equal(false); + expect(ctrl.isShown).to.equal(false); - controller.show(); - expect(controller.isShown).to.equal(true); + await ctrl.show(); + expect(ctrl.isShown).to.equal(true); - controller.hide(); - expect(controller.isShown).to.equal(false); + await ctrl.hide(); + expect(ctrl.isShown).to.equal(false); }); - it('puts the latest shown overlay always on top', () => { - const controller0 = new GlobalOverlayController({ - contentTemplate: () => - html` -

Content0

- `, - }); - const controller1 = new GlobalOverlayController({ - contentTemplate: () => - html` -

Content1

- `, - }); - - controller0.show(); - controller1.show(); - controller0.show(); - - expect(getRenderedContainers().length).to.equal(2); - expect(getRenderedOverlay(0).tagName).to.equal('P'); - expect(getRenderedOverlay(0).textContent).to.equal('Content0'); - expect(getRenderedOverlay(1).tagName).to.equal('P'); - expect(getRenderedOverlay(1).textContent).to.equal('Content1'); - expect(getTopOverlay().textContent).to.equal('Content0'); - }); - - it('does not recreate the overlay elements when calling show multiple times', () => { - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` + it('does not recreate the overlay elements when calling show multiple times', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + contentTemplate: () => html`

Content

`, - }); + }), + ); - controller.show(); + await ctrl.show(); expect(getRenderedContainers().length).to.equal(1); const initialContainer = getRenderedContainer(0); const initialOverlay = getRenderedOverlay(0); - controller.show(); + await ctrl.show(); expect(getRenderedContainers().length).to.equal(1); expect(getRenderedContainer(0)).to.equal(initialContainer); expect(getRenderedOverlay(0)).to.equal(initialOverlay); }); - it('recreates the overlay elements when hiding and showing again', () => { - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` -

Content

+ it('supports .sync(isShown, data)', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + contentTemplate: ({ text = 'default' } = {}) => html` +

${text}

`, - }); + }), + ); - controller.show(); - expect(getRenderedContainers().length).to.equal(1); - const initialContainer = getRenderedContainer(0); - const initialOverlay = getRenderedOverlay(0); - - controller.hide(); - controller.show(); - expect(getRenderedContainers().length).to.equal(1); - expect(getRenderedContainer(0)).to.not.equal(initialContainer); - expect(getRenderedOverlay(0)).to.not.equal(initialOverlay); - }); - - it('supports syncing of shown state, data', () => { - const controller = new GlobalOverlayController({ - contentTemplate: data => - html` -

${data.text}

- `, - }); - - controller.sync({ isShown: true, data: { text: 'hello world' } }); + await ctrl.sync({ isShown: true, data: { text: 'hello world' } }); expect(getRenderedContainers().length).to.equal(1); expect(getRenderedOverlay(0).textContent).to.equal('hello world'); - controller.sync({ isShown: true, data: { text: 'goodbye world' } }); + await ctrl.sync({ isShown: true, data: { text: 'goodbye world' } }); expect(getRenderedContainers().length).to.equal(1); expect(getRenderedOverlay(0).textContent).to.equal('goodbye world'); - controller.sync({ isShown: false, data: { text: 'goodbye world' } }); + await ctrl.sync({ isShown: false, data: { text: 'goodbye world' } }); expect(getRenderedContainers().length).to.equal(0); }); }); describe('elementToFocusAfterHide', () => { - it('focuses body when hiding by default', () => { - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` + it('focuses body when hiding by default', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + viewportConfig: { + placement: 'top-left', + }, + contentTemplate: () => html`
=
`, - }); + }), + ); - controller.show(); + await ctrl.show(); const input = getTopOverlay().querySelector('input'); input.focus(); expect(document.activeElement).to.equal(input); - controller.hide(); + await ctrl.hide(); expect(document.activeElement).to.equal(document.body); }); it('supports elementToFocusAfterHide option to focus it when hiding', async () => { - const input = await fixture( - html` - - `, - ); + const input = await fixture(html` + + `); - const controller = new GlobalOverlayController({ - elementToFocusAfterHide: input, - contentTemplate: () => - html` + const ctrl = overlays.add( + new GlobalOverlayController({ + elementToFocusAfterHide: input, + viewportConfig: { + placement: 'top-left', + }, + contentTemplate: () => html`
`, - }); + }), + ); - controller.show(); + await ctrl.show(); const textarea = getTopOverlay().querySelector('textarea'); textarea.focus(); expect(document.activeElement).to.equal(textarea); - controller.hide(); + await ctrl.hide(); expect(document.activeElement).to.equal(input); }); it('allows to set elementToFocusAfterHide on show', async () => { - const input = await fixture( - html` - - `, - ); + const input = await fixture(html` + + `); - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` + const ctrl = overlays.add( + new GlobalOverlayController({ + viewportConfig: { + placement: 'top-left', + }, + contentTemplate: () => html`
`, - }); + }), + ); - controller.show(input); + await ctrl.show(input); const textarea = getTopOverlay().querySelector('textarea'); textarea.focus(); expect(document.activeElement).to.equal(textarea); - controller.hide(); + await ctrl.hide(); expect(document.activeElement).to.equal(input); }); it('allows to set elementToFocusAfterHide on sync', async () => { - const input = await fixture( - html` - - `, - ); + const input = await fixture(html` + + `); - const controller = new GlobalOverlayController({ - contentTemplate: () => - html` + const ctrl = overlays.add( + new GlobalOverlayController({ + viewportConfig: { + placement: 'top-left', + }, + contentTemplate: () => html`
`, - }); + }), + ); - controller.sync({ isShown: true, elementToFocusAfterHide: input }); + await ctrl.sync({ isShown: true, elementToFocusAfterHide: input }); const textarea = getTopOverlay().querySelector('textarea'); textarea.focus(); expect(document.activeElement).to.equal(textarea); - controller.hide(); + await ctrl.hide(); expect(document.activeElement).to.equal(input); - controller.sync({ isShown: true, elementToFocusAfterHide: input }); + await ctrl.sync({ isShown: true, elementToFocusAfterHide: input }); const textarea2 = getTopOverlay().querySelector('textarea'); textarea2.focus(); expect(document.activeElement).to.equal(textarea2); - controller.sync({ isShown: false }); + await ctrl.sync({ isShown: false }); expect(document.activeElement).to.equal(input); }); }); - describe('hasBackdrop', () => { - it('has no backdrop by default', () => { - const controllerWithoutBackdrop = new GlobalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - }); - controllerWithoutBackdrop.show(); - expect(getRenderedContainer(0).classList.contains('global-overlays__backdrop')).to.be.false; - }); - - it('supports a backdrop option', () => { - const controllerWithoutBackdrop = new GlobalOverlayController({ - hasBackdrop: false, - contentTemplate: () => - html` -

Content

- `, - }); - controllerWithoutBackdrop.show(); - expect(getRenderedContainer(0).classList.contains('global-overlays__backdrop')).to.be.false; - controllerWithoutBackdrop.hide(); - - const controllerWithBackdrop = new GlobalOverlayController({ - hasBackdrop: true, - contentTemplate: () => - html` -

Content

- `, - }); - controllerWithBackdrop.show(); - expect(getRenderedContainer(0).classList.contains('global-overlays__backdrop')).to.be.true; - }); - - it('adds a backdrop to the top most overlay with hasBackdrop enabled', () => { - const controller0 = new GlobalOverlayController({ - hasBackdrop: true, - contentTemplate: () => - html` -

Content0

- `, - }); - controller0.show(); - expect(getRenderedContainer(0).classList.contains('global-overlays__backdrop')).to.be.true; - - const controller1 = new GlobalOverlayController({ - hasBackdrop: false, - contentTemplate: () => - html` -

Content1

- `, - }); - controller1.show(); - expect(getRenderedContainer(0).classList.contains('global-overlays__backdrop')).to.be.true; - expect(getRenderedContainer(1).classList.contains('global-overlays__backdrop')).to.be.false; - - const controller2 = new GlobalOverlayController({ - hasBackdrop: true, - contentTemplate: () => - html` -

Content2

- `, - }); - controller2.show(); - expect(getRenderedContainer(0).classList.contains('global-overlays__backdrop')).to.be.false; - expect(getRenderedContainer(1).classList.contains('global-overlays__backdrop')).to.be.false; - expect(getRenderedContainer(2).classList.contains('global-overlays__backdrop')).to.be.true; - }); - - it('restores the backdrop to the next element with hasBackdrop when hiding', () => { - const controller0 = new GlobalOverlayController({ - hasBackdrop: true, - contentTemplate: () => - html` -

Content0

- `, - }); - controller0.show(); - - const controller1 = new GlobalOverlayController({ - hasBackdrop: false, - contentTemplate: () => - html` -

Content1

- `, - }); - controller1.show(); - - const controller2 = new GlobalOverlayController({ - hasBackdrop: true, - contentTemplate: () => - html` -

Content2

- `, - }); - controller2.show(); - - controller2.hide(); - - expect(getRenderedContainer(0).classList.contains('global-overlays__backdrop')).to.be.true; - expect(getRenderedContainer(1).classList.contains('global-overlays__backdrop')).to.be.false; - }); - }); - - describe('isBlocking', () => { - it('prevents showing of other overlays', () => { - const controller0 = new GlobalOverlayController({ - isBlocking: false, - contentTemplate: () => - html` -

Content0

- `, - }); - controller0.show(); - - const controller1 = new GlobalOverlayController({ - isBlocking: false, - contentTemplate: () => - html` -

Content1

- `, - }); - controller1.show(); - - const controller2 = new GlobalOverlayController({ - isBlocking: true, - contentTemplate: () => - html` -

Content2

- `, - }); - controller2.show(); - - const controller3 = new GlobalOverlayController({ - isBlocking: false, - contentTemplate: () => - html` -

Content3

- `, - }); - controller3.show(); - - expect(window.getComputedStyle(getRenderedContainer(0)).display).to.equal('none'); - expect(window.getComputedStyle(getRenderedContainer(1)).display).to.equal('none'); - expect(window.getComputedStyle(getRenderedContainer(2)).display).to.equal('block'); - expect(window.getComputedStyle(getRenderedContainer(3)).display).to.equal('none'); - }); - }); - - describe('trapsKeyboardFocus (for a11y)', () => { - it('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', () => { - const controller = new GlobalOverlayController({ - trapsKeyboardFocus: true, - contentTemplate: () => - html` -

Content

- `, - }); - - // show+hide are needed to create a root node - controller.show(); - controller.hide(); - - const sibling1 = document.createElement('div'); - const sibling2 = document.createElement('div'); - document.body.insertBefore(sibling1, getRootNode()); - document.body.appendChild(sibling2); - - controller.show(); - - [sibling1, sibling2].forEach(sibling => { - expect(sibling.getAttribute('aria-hidden')).to.equal('true'); - expect(sibling.hasAttribute('inert')).to.be.true; - }); - expect(getRenderedOverlay(0).hasAttribute('aria-hidden')).to.be.false; - expect(getRenderedOverlay(0).hasAttribute('inert')).to.be.false; - - controller.hide(); - - [sibling1, sibling2].forEach(sibling => { - expect(sibling.hasAttribute('aria-hidden')).to.be.false; - expect(sibling.hasAttribute('inert')).to.be.false; - }); - - // cleanup - document.body.removeChild(sibling1); - document.body.removeChild(sibling2); - }); - - /** - * style.userSelect: - * - chrome: 'none' - * - rest: undefined - * - * style.pointerEvents: - * - chrome: auto - * - IE11: visiblePainted - */ - it('disables pointer events and selection on inert elements', async () => { - const controller = new GlobalOverlayController({ - trapsKeyboardFocus: true, - contentTemplate: () => - html` -

Content

- `, - }); - - // show+hide are needed to create a root node - controller.show(); - controller.hide(); - - const sibling1 = document.createElement('div'); - const sibling2 = document.createElement('div'); - document.body.insertBefore(sibling1, getRootNode()); - document.body.appendChild(sibling2); - - controller.show(); - - [sibling1, sibling2].forEach(sibling => { - expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['none', undefined]); - expect(window.getComputedStyle(sibling).pointerEvents).to.equal('none'); - }); - expect(window.getComputedStyle(getRenderedOverlay(0)).userSelect).to.be.oneOf([ - 'auto', - undefined, - ]); - expect(window.getComputedStyle(getRenderedOverlay(0)).pointerEvents).to.be.oneOf([ - 'auto', - 'visiblePainted', - ]); - - controller.hide(); - - [sibling1, sibling2].forEach(sibling => { - expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['auto', undefined]); - expect(window.getComputedStyle(sibling).pointerEvents).to.be.oneOf([ - 'auto', - 'visiblePainted', - ]); - }); - - // cleanup - document.body.removeChild(sibling1); - document.body.removeChild(sibling2); - }); - - it('focuses the overlay on show', () => { - const controller = new GlobalOverlayController({ - trapsKeyboardFocus: true, - contentTemplate: () => - html` -

Content

- `, - }); - controller.show(); - expect(getRenderedOverlay(0)).to.equal(document.activeElement); - }); - - it('keeps focus within the overlay e.g. you can not tab out by accident', async () => { - const controller = new GlobalOverlayController({ - trapsKeyboardFocus: true, - contentTemplate: () => - html` -
- `, - }); - controller.show(); - - const elOutside = await fixture( - html` - - `, - ); - const input1 = getRenderedOverlay(0).querySelectorAll('input')[0]; - const input2 = getRenderedOverlay(0).querySelectorAll('input')[1]; - - input2.focus(); - // this mimics a tab within the contain-focus system used - const event = new CustomEvent('keydown', { detail: 0, bubbles: true }); - event.keyCode = keyCodes.tab; - window.dispatchEvent(event); - - expect(elOutside).to.not.equal(document.activeElement); - expect(input1).to.equal(document.activeElement); - }); - - it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => { - const controller = new GlobalOverlayController({ - trapsKeyboardFocus: false, - contentTemplate: () => - html` -
- `, - }); - controller.show(); - - const elOutside = await fixture( - html` - - `, - ); - const input = getRenderedOverlay(0).querySelector('input'); - - input.focus(); - simulateTab(); - - expect(elOutside).to.equal(document.activeElement); - }); - - it.skip('keeps focus within overlay with multiple overlays with all traps on true', async () => { - // TODO: find a way to test it - const controller0 = new GlobalOverlayController({ - trapsKeyboardFocus: true, - contentTemplate: () => - html` -
- Link0 -
- `, - }); - - const controller1 = new GlobalOverlayController({ - trapsKeyboardFocus: true, - contentTemplate: () => - html` -
- Link1 -
- `, - }); - - controller0.show(); - controller1.show(); - - simulateTab(); - expect(getDeepActiveElement().id).to.equal('input1'); - simulateTab(); - expect(getDeepActiveElement().id).to.equal('button1'); - simulateTab(); - expect(getDeepActiveElement().id).to.equal('input1'); - }); - }); - describe('preventsScroll', () => { it('prevent scrolling the background', async () => { - const controller = new GlobalOverlayController({ - preventsScroll: true, - contentTemplate: () => - html` + const ctrl = overlays.add( + new GlobalOverlayController({ + preventsScroll: true, + contentTemplate: () => html`

Content

`, - }); + }), + ); - controller.show(); - controller.updateComplete; + await ctrl.show(); + ctrl.updateComplete; expect(getComputedStyle(document.body).overflow).to.equal('hidden'); - controller.hide(); - controller.updateComplete; + await ctrl.hide(); + ctrl.updateComplete; expect(getComputedStyle(document.body).overflow).to.equal('visible'); }); }); - describe('hidesOnEsc', () => { - it('hides when Escape is pressed', async () => { + describe('hasBackdrop', () => { + it('has no backdrop by default', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + contentTemplate: () => html` +

Content

+ `, + }), + ); + await ctrl.show(); + expect(ctrl.backdropNode).to.be.undefined; + }); + + it('supports a backdrop option', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + hasBackdrop: false, + contentTemplate: () => html` +

Content

+ `, + }), + ); + await ctrl.show(); + expect(ctrl.backdropNode).to.be.undefined; + await ctrl.hide(); + + const controllerWithBackdrop = overlays.add( + new GlobalOverlayController({ + hasBackdrop: true, + contentTemplate: () => html` +

Content

+ `, + }), + ); + await controllerWithBackdrop.show(); + expect(controllerWithBackdrop.backdropNode).to.have.class('global-overlays__backdrop'); + }); + + it('reenables the backdrop when shown/hidden/shown', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + hasBackdrop: true, + contentTemplate: () => html` +

Content

+ `, + }), + ); + await ctrl.show(); + expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop'); + await ctrl.hide(); + await ctrl.show(); + expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop'); + }); + }); + + describe('viewportConfig', () => { + it('places the overlay in center by default', async () => { const controller = new GlobalOverlayController({ - hidesOnEsc: true, contentTemplate: () => html`

Content

@@ -694,10 +349,38 @@ describe('GlobalOverlayController', () => { }); controller.show(); - expect(getRenderedContainers().length).to.equal(1); + expect(controller.overlayContainerPlacementClass).to.equal( + 'global-overlays__overlay-container--center', + ); + }); - keyUpOn(getRenderedContainer(0), keyCodes.escape); - expect(getRenderedContainers().length).to.equal(0); + it('can set the placement relative to the viewport ', async () => { + const placementMap = [ + 'top-left', + 'top', + 'top-right', + 'right', + 'bottom-right', + 'bottom', + 'bottom-left', + 'left', + 'center', + ]; + placementMap.forEach(viewportPlacement => { + const controller = new GlobalOverlayController({ + viewportConfig: { + placement: viewportPlacement, + }, + contentTemplate: () => + html` +

Content

+ `, + }); + controller.show(); + expect(controller.overlayContainerPlacementClass).to.equal( + `global-overlays__overlay-container--${viewportPlacement}`, + ); + }); }); }); }); diff --git a/packages/overlays/test/LocalOverlayController.test.js b/packages/overlays/test/LocalOverlayController.test.js index cc7416c81..6a31a9d52 100644 --- a/packages/overlays/test/LocalOverlayController.test.js +++ b/packages/overlays/test/LocalOverlayController.test.js @@ -1,195 +1,124 @@ import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing'; -import sinon from 'sinon'; import Popper from 'popper.js/dist/esm/popper.min.js'; -import { keyUpOn } from '@polymer/iron-test-helpers/mock-interactions.js'; -import { LionLitElement } from '@lion/core/src/LionLitElement.js'; import { keyCodes } from '../src/utils/key-codes.js'; import { simulateTab } from '../src/utils/simulate-tab.js'; import { LocalOverlayController } from '../src/LocalOverlayController.js'; -import { overlays } from '../src/overlays.js'; +import { runBaseOverlaySuite } from '../test-suites/BaseOverlayController.suite.js'; + +/** + * @desc Compensates for browsers that use floats in output + * - from: 'transform3d(12.25px, 6.75px, 0px)' + * - to: 'transform3d(12px, 7px, 0px)' + * @param {string} cssValue + */ +export function normalizeTransformStyle(cssValue) { + // eslint-disable-next-line no-unused-vars + const [_, transformType, positionPart] = cssValue.match(/(.*)\((.*?)\)/); + const normalizedNumbers = positionPart + .split(',') + .map(p => Math.round(Number(p.replace('px', '')))); + return `${transformType}(${normalizedNumbers + .map((n, i) => `${n}px${normalizedNumbers.length - 1 === i ? '' : ', '}`) + .join('')})`; +} describe('LocalOverlayController', () => { + describe('extends BaseOverlayController', () => { + runBaseOverlaySuite((...args) => new LocalOverlayController(...args)); + }); + describe('templates', () => { - it('creates a controller with methods: show, hide, sync and syncInvoker', () => { - const controller = new LocalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, + it('creates a controller with methods: show, hide, sync and syncInvoker', async () => { + const invokerNode = await fixture(html` +
Invoker
+ `); + const ctrl = new LocalOverlayController({ + contentTemplate: () => html` +
Content
+ `, + invokerNode, }); - expect(controller.show).to.be.a('function'); - expect(controller.hide).to.be.a('function'); - expect(controller.sync).to.be.a('function'); - expect(controller.syncInvoker).to.be.a('function'); + expect(ctrl.show).to.be.a('function'); + expect(ctrl.hide).to.be.a('function'); + expect(ctrl.sync).to.be.a('function'); + expect(ctrl.syncInvoker).to.be.a('function'); }); - it('will render holders for invoker and content', async () => { - const controller = new LocalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, + it('renders holders for invoker and content', async () => { + const invokerNode = await fixture(html` +
Invoker
+ `); + const ctrl = new LocalOverlayController({ + contentTemplate: () => html` +
Content
+ `, + invokerNode, }); const el = await fixture(html`
- ${controller.invoker} ${controller.content} -
- `); - expect(el.querySelectorAll('div')[0].textContent.trim()).to.equal('Invoker'); - controller.show(); - expect(el.querySelectorAll('div')[1].textContent.trim()).to.equal('Content'); - }); - - it('will add/remove the content on show/hide', async () => { - const controller = new LocalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, - }); - const el = await fixture(html` -
- ${controller.invoker} ${controller.content} + ${ctrl.invoker} ${ctrl.content}
`); - expect(el.querySelectorAll('div')[1].textContent.trim()).to.equal(''); - - controller.show(); - expect(el.querySelectorAll('div')[1].textContent.trim()).to.equal('Content'); - - controller.hide(); - expect(el.querySelectorAll('div')[1].textContent.trim()).to.equal(''); - }); - - it('will hide and show html nodes provided to overlay', async () => { - const tagString = defineCE( - class extends LionLitElement { - render() { - return html` - - `; - } - }, - ); - - const element = unsafeStatic(tagString); - const elem = await fixture(html` - <${element}> -
content
-
- <${element}> - `); - - const controller = overlays.add( - new LocalOverlayController({ - hidesOnEsc: true, - hidesOnOutsideClick: true, - contentNode: elem.querySelector('[slot="content"]'), - invokerNode: elem.querySelector('[slot="invoker"]'), - }), - ); - - expect(elem.querySelector('[slot="content"]').style.display).to.equal('none'); - controller.show(); - expect(elem.querySelector('[slot="content"]').style.display).to.equal('inline-block'); - controller.hide(); - expect(elem.querySelector('[slot="content"]').style.display).to.equal('none'); + expect(el.querySelector('#invoke').textContent.trim()).to.equal('Invoker'); + await ctrl.show(); + expect(el.querySelector('#content').textContent.trim()).to.equal('Content'); }); it('exposes isShown state for reading', async () => { - const controller = new LocalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, + const invokerNode = await fixture('
Invoker
'); + const ctrl = new LocalOverlayController({ + contentTemplate: () => html` +
Content
+ `, + invokerNode, }); await fixture(html`
- ${controller.invoker} ${controller.content} + ${ctrl.invoker} ${ctrl.content}
`); - expect(controller.isShown).to.equal(false); - controller.show(); - expect(controller.isShown).to.equal(true); - controller.hide(); - expect(controller.isShown).to.equal(false); + expect(ctrl.isShown).to.equal(false); + await ctrl.show(); + expect(ctrl.isShown).to.equal(true); + await ctrl.hide(); + expect(ctrl.isShown).to.equal(false); }); - it('can update the invoker data', async () => { - const controller = new LocalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: (data = { text: 'foo' }) => - html` - - `, + // deprecated + it('@deprecated can use a .invokerTemplate and .syncInvoker', async () => { + const ctrl = new LocalOverlayController({ + contentTemplate: () => html` +
Content
+ `, + invokerTemplate: (data = { text: 'foo' }) => html` +
${data.text}
+ `, }); - expect(controller.invoker.textContent.trim()).to.equal('foo'); - controller.syncInvoker({ data: { text: 'bar' } }); - expect(controller.invoker.textContent.trim()).to.equal('bar'); + expect(ctrl.invoker.textContent.trim()).to.equal('foo'); + ctrl.syncInvoker({ data: { text: 'bar' } }); + expect(ctrl.invoker.textContent.trim()).to.equal('bar'); }); it('can synchronize the content data', async () => { - const controller = new LocalOverlayController({ - contentTemplate: data => - html` -

${data.text}

- `, - invokerTemplate: () => - html` - - `, + const invokerNode = await fixture('
Invoker
'); + const ctrl = new LocalOverlayController({ + contentTemplate: ({ text = 'fallback' } = {}) => html` +
${text}
+ `, + invokerNode, }); - await controller.show(); - controller.sync({ data: { text: 'foo' } }); - expect(controller.content.textContent.trim()).to.equal('foo'); + await ctrl.show(); + await ctrl.sync({ data: { text: 'foo' } }); + expect(ctrl.content.textContent.trim()).to.equal('foo'); - controller.sync({ data: { text: 'bar' } }); - expect(controller.content.textContent.trim()).to.equal('bar'); - }); - - it.skip('can reuse an existing node for the invoker (disables syncInvoker())', async () => { - const controller = new LocalOverlayController({ - contentTemplate: () => - html` -

Content

- `, - invokerReference: null, // TODO: invokerReference - }); - await fixture(` -
- - ${controller.content} -
- `); - expect(controller.invoker.textContent.trim()).to.equal('Invoker'); - controller.show(); - expect(controller.content.textContent.trim()).to.equal('Content'); + await ctrl.sync({ data: { text: 'bar' } }); + expect(ctrl.content.textContent.trim()).to.equal('bar'); }); }); @@ -197,69 +126,87 @@ describe('LocalOverlayController', () => { // the test runner from interfering. describe('positioning', () => { it('creates a popper instance on the controller when shown, keeps it when hidden', async () => { - const controller = new LocalOverlayController({ + const invokerNode = await fixture( + html` +
+ `, + ); + const ctrl = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, }); - await controller.show(); - expect(controller._popper) + await ctrl.show(); + expect(ctrl._popper) .to.be.an.instanceof(Popper) .and.have.property('modifiers'); - controller.hide(); - expect(controller._popper) + await ctrl.hide(); + expect(ctrl._popper) .to.be.an.instanceof(Popper) .and.have.property('modifiers'); }); it('positions correctly', async () => { // smoke test for integration of popper - const controller = new LocalOverlayController({ + const invokerNode = await fixture(html` +
Invoker
+ `); + const ctrl = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
my content
`, + invokerNode, }); + await fixture(html` + ${invokerNode}${ctrl.content} + `); - await controller.show(); - expect(controller.content.firstElementChild.style.transform).to.equal( - 'translate3d(16px, 16px, 0px)', + await ctrl.show(); + + expect(normalizeTransformStyle(ctrl.contentNode.style.transform)).to.equal( + // TODO: check if 'translate3d(16px, 16px, 0px)' would be more appropriate + 'translate3d(16px, 28px, 0px)', '16px displacement is expected due to both horizontal and vertical viewport margin', ); }); it('uses top as the default placement', async () => { - const controller = new LocalOverlayController({ + let ctrl; + const invokerNode = await fixture(html` +
ctrl.show()}>
+ `); + ctrl = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, }); await fixture(html`
- ${controller.invoker} ${controller.content} + ${ctrl.invoker} ${ctrl.content}
`); - await controller.show(); - const contentChild = controller.content.firstElementChild; + await ctrl.show(); + const contentChild = ctrl.content.firstElementChild; expect(contentChild.getAttribute('x-placement')).to.equal('top'); }); it('positions to preferred place if placement is set and space is available', async () => { - const controller = new LocalOverlayController({ + let controller; + const invokerNode = await fixture(html` +
controller.show()} + >
+ `); + + controller = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, popperConfig: { placement: 'left-start', }, @@ -276,36 +223,44 @@ describe('LocalOverlayController', () => { }); it('positions to different place if placement is set and no space is available', async () => { - const controller = new LocalOverlayController({ + let ctrl; + const invokerNode = await fixture(html` +
ctrl.show()}>
+ `); + ctrl = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, popperConfig: { placement: 'top-start', }, }); await fixture(`
- ${controller.invoker} ${controller.content} + ${ctrl.invoker} ${ctrl.content}
`); - await controller.show(); - const contentChild = controller.content.firstElementChild; + await ctrl.show(); + const contentChild = ctrl.content.firstElementChild; expect(contentChild.getAttribute('x-placement')).to.equal('bottom-start'); }); it('allows the user to override default Popper modifiers', async () => { - const controller = new LocalOverlayController({ + let controller; + const invokerNode = await fixture(html` +
controller.show()} + >
+ `); + controller = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, popperConfig: { modifiers: { keepTogether: { @@ -332,44 +287,20 @@ describe('LocalOverlayController', () => { expect(offset.offset).to.equal('0, 16px'); }); - it('updates popperConfig even when overlay is closed', async () => { - const controller = new LocalOverlayController({ - contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - - `, - popperConfig: { - placement: 'top', - }, - }); - await fixture(html` -
- ${controller.invoker} ${controller.content} -
- `); - await controller.show(); - const contentChild = controller.content.firstElementChild; - expect(contentChild.getAttribute('x-placement')).to.equal('top'); - - controller.hide(); - await controller.updatePopperConfig({ placement: 'bottom' }); - await controller.show(); - expect(controller._popper.options.placement).to.equal('bottom'); - }); - it('positions the popper element correctly on show', async () => { - const controller = new LocalOverlayController({ + let controller; + const invokerNode = await fixture(html` +
controller.show()} + >
+ `); + controller = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, popperConfig: { placement: 'top', }, @@ -381,29 +312,37 @@ describe('LocalOverlayController', () => { `); await controller.show(); + let contentChild = controller.content.firstElementChild; - expect(contentChild.style.transform).to.equal( - 'translate3d(10px, -60px, 0px)', + expect(normalizeTransformStyle(contentChild.style.transform)).to.equal( + 'translate3d(10px, -28px, 0px)', 'Popper positioning values', ); - controller.hide(); + await controller.hide(); await controller.show(); contentChild = controller.content.firstElementChild; - expect(contentChild.style.transform).to.equal( - 'translate3d(10px, -60px, 0px)', + expect(normalizeTransformStyle(contentChild.style.transform)).to.equal( + 'translate3d(10px, -28px, 0px)', 'Popper positioning values should be identical after hiding and showing', ); }); + // TODO: dom get's removed when hidden so no dom node to update placement it('updates placement properly even during hidden state', async () => { - const controller = new LocalOverlayController({ + let controller; + const invokerNode = await fixture(html` +
controller.show()} + >
+ `); + controller = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, popperConfig: { placement: 'top', modifiers: { @@ -422,12 +361,12 @@ describe('LocalOverlayController', () => { await controller.show(); let contentChild = controller.content.firstElementChild; - expect(contentChild.style.transform).to.equal( - 'translate3d(10px, -62px, 0px)', + expect(normalizeTransformStyle(contentChild.style.transform)).to.equal( + 'translate3d(10px, -30px, 0px)', 'Popper positioning values', ); - controller.hide(); + await controller.hide(); await controller.updatePopperConfig({ modifiers: { offset: { @@ -439,22 +378,24 @@ describe('LocalOverlayController', () => { await controller.show(); contentChild = controller.content.firstElementChild; expect(controller._popper.options.modifiers.offset.offset).to.equal('0, 20px'); - expect(contentChild.style.transform).to.equal( - 'translate3d(10px, -72px, 0px)', + expect(normalizeTransformStyle(contentChild.style.transform)).to.equal( + 'translate3d(10px, -40px, 0px)', 'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset', ); }); it('updates positioning correctly during shown state when config gets updated', async () => { - const controller = new LocalOverlayController({ + let controller; + const invokerNode = await fixture(html` +
controller.show()}> + Invoker +
+ `); + controller = new LocalOverlayController({ contentTemplate: () => html` -

- `, - invokerTemplate: () => html` - +
`, + invokerNode, popperConfig: { placement: 'top', modifiers: { @@ -473,8 +414,8 @@ describe('LocalOverlayController', () => { await controller.show(); const contentChild = controller.content.firstElementChild; - expect(contentChild.style.transform).to.equal( - 'translate3d(10px, -62px, 0px)', + expect(normalizeTransformStyle(contentChild.style.transform)).to.equal( + 'translate3d(10px, -30px, 0px)', 'Popper positioning values', ); @@ -486,79 +427,99 @@ describe('LocalOverlayController', () => { }, }, }); - expect(contentChild.style.transform).to.equal( - 'translate3d(10px, -72px, 0px)', + expect(normalizeTransformStyle(contentChild.style.transform)).to.equal( + 'translate3d(10px, -40px, 0px)', 'Popper positioning Y value should be 10 less than previous, due to the added extra 10px offset', ); }); - it('can set the contentNode minWidth as the invokerNode width', () => { - const controller = new LocalOverlayController({ + it('can set the contentNode minWidth as the invokerNode width', async () => { + const invokerNode = await fixture( + '
invoker
', + ); + const ctrl = new LocalOverlayController({ inheritsReferenceObjectWidth: 'min', + contentTemplate: () => + html` +
content
+ `, + invokerNode, }); - expect(controller.contentNode.style.minWidth).to.equal(controller.invokerNode.style.width); + await ctrl.show(); + expect(ctrl.contentNode.style.minWidth).to.equal('60px'); }); - it('can set the contentNode maxWidth as the invokerNode width', () => { - const controller = new LocalOverlayController({ + it('can set the contentNode maxWidth as the invokerNode width', async () => { + const invokerNode = await fixture( + '
invoker
', + ); + const ctrl = new LocalOverlayController({ inheritsReferenceObjectWidth: 'max', + contentTemplate: () => + html` +
content
+ `, + invokerNode, }); - expect(controller.contentNode.style.maxWidth).to.equal(controller.invokerNode.style.width); + await ctrl.show(); + expect(ctrl.contentNode.style.maxWidth).to.equal('60px'); }); - it('can set the contentNode width as the invokerNode width', () => { - const controller = new LocalOverlayController({ + it('can set the contentNode width as the invokerNode width', async () => { + const invokerNode = await fixture( + '
invoker
', + ); + const ctrl = new LocalOverlayController({ inheritsReferenceObjectWidth: 'full', + contentTemplate: () => + html` +
content
+ `, + invokerNode, }); - expect(controller.contentNode.style.width).to.equal(controller.invokerNode.style.width); + await ctrl.show(); + expect(ctrl.contentNode.style.width).to.equal('60px'); }); }); describe('a11y', () => { - it('adds and removes aria-expanded on invoker', async () => { - const controller = new LocalOverlayController({ + it('adds and removes [aria-expanded] on invoker', async () => { + const invokerNode = await fixture('
invoker
'); + const ctrl = new LocalOverlayController({ contentTemplate: () => html` -

Content

- `, - invokerTemplate: () => - html` - +
Content
`, + invokerNode, }); - - expect(controller.invokerNode.getAttribute('aria-controls')).to.contain( - controller.content.id, - ); - expect(controller.invokerNode.getAttribute('aria-expanded')).to.equal('false'); - controller.show(); - expect(controller.invokerNode.getAttribute('aria-expanded')).to.equal('true'); - controller.hide(); - expect(controller.invokerNode.getAttribute('aria-expanded')).to.equal('false'); + expect(ctrl.invokerNode.getAttribute('aria-controls')).to.contain(ctrl.content.id); + expect(ctrl.invokerNode).to.have.attribute('aria-expanded', 'false'); + await ctrl.show(); + expect(ctrl.invokerNode).to.have.attribute('aria-expanded', 'true'); + await ctrl.hide(); + expect(ctrl.invokerNode).to.have.attribute('aria-expanded', 'false'); }); it('traps the focus via option { trapsKeyboardFocus: true }', async () => { - const controller = new LocalOverlayController({ + const invokerNode = await fixture(''); + const ctrl = new LocalOverlayController({ contentTemplate: () => html`
Anchor
`, - invokerTemplate: () => - html` - - `, + invokerNode, trapsKeyboardFocus: true, }); // make sure we're connected to the dom await fixture(html` - ${controller.invoker}${controller.content} + ${invokerNode}${ctrl.content} `); - controller.show(); + await ctrl.show(); - const elOutside = await fixture(``); - const [el1, el2] = [].slice.call(controller.contentNode.querySelectorAll('[id]')); + const elOutside = await fixture(`
click me
`); + const [el1, el2] = [].slice.call(ctrl.contentNode.querySelectorAll('[id]')); el2.focus(); // this mimics a tab within the contain-focus system used const event = new CustomEvent('keydown', { detail: 0, bubbles: true }); @@ -571,26 +532,26 @@ describe('LocalOverlayController', () => { it('traps the focus via option { trapsKeyboardFocus: true } when using contentNode', async () => { const invokerNode = await fixture(''); - const contentNode = await fixture(` + const contentNode = await fixture(html`
Anchor
`); - const controller = new LocalOverlayController({ + const ctrl = new LocalOverlayController({ contentNode, invokerNode, trapsKeyboardFocus: true, }); // make sure we're connected to the dom await fixture(html` - ${controller.invoker}${controller.content} + ${ctrl.invoker}${ctrl.content} `); - controller.show(); + await ctrl.show(); - const elOutside = await fixture(``); - const [el1, el2] = [].slice.call(controller.contentNode.querySelectorAll('[id]')); + const elOutside = await fixture(`
click me
`); + const [el1, el2] = [].slice.call(ctrl.contentNode.querySelectorAll('[id]')); el2.focus(); // this mimics a tab within the contain-focus system used @@ -603,24 +564,23 @@ describe('LocalOverlayController', () => { }); it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => { - const controller = new LocalOverlayController({ + const invokerNode = await fixture(''); + const ctrl = new LocalOverlayController({ contentTemplate: () => html`
`, - invokerTemplate: () => html` - - `, + invokerNode, trapsKeyboardFocus: false, }); // make sure we're connected to the dom await fixture(html` - ${controller.invoker}${controller.content} + ${ctrl.invoker}${ctrl.content} `); const elOutside = await fixture(``); - controller.show(); - const el1 = controller.content.querySelector('button'); + await ctrl.show(); + const el1 = ctrl.content.querySelector('button'); el1.focus(); simulateTab(); @@ -628,119 +588,54 @@ describe('LocalOverlayController', () => { }); }); - describe('hidesOnEsc', () => { - it('hides when [escape] is pressed', async () => { - const ctrl = new LocalOverlayController({ - hidesOnEsc: true, - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, - }); - await fixture( - html` - ${ctrl.invoker}${ctrl.content} - `, - ); - ctrl.show(); - - keyUpOn(ctrl.contentNode, keyCodes.escape); - ctrl.updateComplete; - expect(ctrl.isShown).to.equal(false); - }); - - it('stays shown when [escape] is pressed on outside element', async () => { - const ctrl = new LocalOverlayController({ - hidesOnEsc: true, - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, - }); - await fixture( - html` - ${ctrl.invoker}${ctrl.content} - `, - ); - ctrl.show(); - - keyUpOn(document, keyCodes.escape); - ctrl.updateComplete; - expect(ctrl.isShown).to.equal(true); - }); - }); - describe('hidesOnOutsideClick', () => { it('hides on outside click', async () => { - const controller = new LocalOverlayController({ + const invokerNode = await fixture('
Invoker
'); + const ctrl = new LocalOverlayController({ hidesOnOutsideClick: true, - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, - }); - await fixture( - html` - ${controller.invoker}${controller.content} + contentTemplate: () => html` +
Content
`, - ); - const { content } = controller; - controller.show(); - expect(content.textContent.trim()).to.equal('Content'); + invokerNode, + }); + await fixture(html` + ${invokerNode}${ctrl.content} + `); + await ctrl.show(); document.body.click(); await aTimeout(); - expect(content.textContent.trim()).to.equal(''); + expect(ctrl.isShown).to.be.false; }); it('doesn\'t hide on "inside" click', async () => { + const invokerNode = await fixture(html` + + `); const ctrl = new LocalOverlayController({ hidesOnOutsideClick: true, - contentTemplate: () => - html` -

Content

- `, - invokerTemplate: () => - html` - - `, + contentTemplate: () => html` +
Content
+ `, + invokerNode, }); - const { content, invoker } = ctrl; await fixture(html` - ${invoker}${content} + ${invokerNode}${ctrl.content} `); + await ctrl.show(); - // Don't hide on first invoker click + // Don't hide on invoker click ctrl.invokerNode.click(); await aTimeout(); - expect(ctrl.isShown).to.equal(true); + expect(ctrl.isShown).to.be.true; // Don't hide on inside (content) click ctrl.contentNode.click(); await aTimeout(); - expect(ctrl.isShown).to.equal(true); - - // Don't hide on invoker click when shown - ctrl.invokerNode.click(); - await aTimeout(); - expect(ctrl.isShown).to.equal(true); + expect(ctrl.isShown).to.be.true; // Works as well when clicked content element lives in shadow dom - ctrl.show(); - await aTimeout(); - const tag = defineCE( + const tagString = defineCE( class extends HTMLElement { constructor() { super(); @@ -752,35 +647,46 @@ describe('LocalOverlayController', () => { } }, ); - const shadowEl = document.createElement(tag); - content.appendChild(shadowEl); - shadowEl.shadowRoot.querySelector('button').click(); + const tag = unsafeStatic(tagString); + ctrl.contentTemplate = () => + html` +
+
Content
+ <${tag}> +
+ `; + + // Don't hide on inside shadowDom click + ctrl.content + .querySelector(tagString) + .shadowRoot.querySelector('button') + .click(); + await aTimeout(); - expect(ctrl.isShown).to.equal(true); + expect(ctrl.isShown).to.be.true; // Important to check if it can be still shown after, because we do some hacks inside - ctrl.hide(); - expect(ctrl.isShown).to.equal(true); - ctrl.show(); - expect(ctrl.isShown).to.equal(true); + await ctrl.hide(); + expect(ctrl.isShown).to.be.false; + await ctrl.show(); + expect(ctrl.isShown).to.be.true; }); it('works with 3rd party code using "event.stopPropagation()" on bubble phase', async () => { + const invokerNode = await fixture(html` +
Invoker
+ `); const ctrl = new LocalOverlayController({ hidesOnOutsideClick: true, contentTemplate: () => html` -

Content

- `, - invokerTemplate: () => - html` - +
Content
`, + invokerNode, }); - const { content, invoker } = ctrl; const dom = await fixture(`
- +
{ + const invokerNode = await fixture(html` +
Invoker
+ `); const ctrl = new LocalOverlayController({ hidesOnOutsideClick: true, contentTemplate: () => html` -

Content

- `, - invokerTemplate: () => - html` - +
Content
`, + invokerNode, }); - const { content, invoker } = ctrl; const dom = await fixture(`
- +
ctrl.toggle()}">Invoker + `); + ctrl = new LocalOverlayController({ hidesOnOutsideClick: true, contentTemplate: () => html` -

Content

- `, - invokerTemplate: () => - html` - +
Content
`, + invokerNode, }); - const { content, invoker, invokerNode } = ctrl; + const { content, invoker, invokerNode: iNode } = ctrl; await fixture( html` ${invoker}${content} @@ -875,42 +781,19 @@ describe('LocalOverlayController', () => { ); // Show content on first invoker click - invokerNode.click(); + iNode.click(); await aTimeout(); expect(ctrl.isShown).to.equal(true); // Hide content on click when shown - invokerNode.click(); + iNode.click(); await aTimeout(); expect(ctrl.isShown).to.equal(false); - // Show contnet on invoker click when hidden - invokerNode.click(); + // Show content on invoker click when hidden + iNode.click(); await aTimeout(); expect(ctrl.isShown).to.equal(true); }); }); - - describe('events', () => { - it('fires "show" event once overlay becomes shown', async () => { - const showSpy = sinon.spy(); - const ctrl = new LocalOverlayController(); - ctrl.addEventListener('show', showSpy); - await ctrl.show(); - expect(showSpy.callCount).to.equal(1); - await ctrl.show(); - expect(showSpy.callCount).to.equal(1); - }); - - it('fires "hide" event once overlay becomes hidden', async () => { - const hideSpy = sinon.spy(); - const ctrl = new LocalOverlayController(); - ctrl.addEventListener('hide', hideSpy); - ctrl.hide(); - expect(hideSpy.callCount).to.equal(0); - await ctrl.show(); - ctrl.hide(); - expect(hideSpy.callCount).to.equal(1); - }); - }); }); diff --git a/packages/overlays/test/ManagedGlobalOverlayController.test.js b/packages/overlays/test/ManagedGlobalOverlayController.test.js new file mode 100644 index 000000000..86462d8c2 --- /dev/null +++ b/packages/overlays/test/ManagedGlobalOverlayController.test.js @@ -0,0 +1,281 @@ +import { expect, html } from '@open-wc/testing'; + +import { GlobalOverlayController } from '../src/GlobalOverlayController.js'; +import { overlays } from '../src/overlays.js'; + +function getRootNode() { + return document.querySelector('.global-overlays'); +} + +function getRenderedContainers() { + const rootNode = getRootNode(); + return rootNode ? Array.from(rootNode.children) : []; +} + +function getRenderedContainer(index) { + return getRenderedContainers()[index]; +} + +function getRenderedOverlay(index) { + const container = getRenderedContainer(index); + return container ? container.children[0] : null; +} + +function cleanup() { + document.body.removeAttribute('style'); + overlays.teardown(); +} + +describe('Managed GlobalOverlayController', () => { + afterEach(cleanup); + + describe('hasBackdrop', () => { + it('adds and stacks backdrops if .hasBackdrop is enabled', async () => { + const ctrl0 = overlays.add( + new GlobalOverlayController({ + hasBackdrop: true, + contentTemplate: () => html` +

Content0

+ `, + }), + ); + await ctrl0.show(); + expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); + + const ctrl1 = overlays.add( + new GlobalOverlayController({ + hasBackdrop: false, + contentTemplate: () => html` +

Content1

+ `, + }), + ); + await ctrl1.show(); + expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); + expect(ctrl1.backdropNode).to.be.undefined; + + const ctrl2 = overlays.add( + new GlobalOverlayController({ + hasBackdrop: true, + contentTemplate: () => html` +

Content2

+ `, + }), + ); + await ctrl2.show(); + + expect(ctrl0.backdropNode).to.have.class('global-overlays__backdrop'); + expect(ctrl1.backdropNode).to.be.undefined; + expect(ctrl2.backdropNode).to.have.class('global-overlays__backdrop'); + }); + }); + + describe('isBlocking', () => { + it('prevents showing of other overlays', async () => { + const ctrl0 = overlays.add( + new GlobalOverlayController({ + isBlocking: false, + contentTemplate: () => html` +

Content0

+ `, + }), + ); + await ctrl0.show(); + + const ctrl1 = overlays.add( + new GlobalOverlayController({ + isBlocking: false, + contentTemplate: () => html` +

Content1

+ `, + }), + ); + await ctrl1.show(); + + const ctrl2 = overlays.add( + new GlobalOverlayController({ + isBlocking: true, + contentTemplate: () => html` +

Content2

+ `, + }), + ); + await ctrl2.show(); + + const ctrl3 = overlays.add( + new GlobalOverlayController({ + isBlocking: false, + contentTemplate: () => html` +

Content3

+ `, + }), + ); + await ctrl3.show(); + + expect(getRenderedOverlay(0)).to.not.be.displayed; + expect(getRenderedOverlay(1)).to.not.be.displayed; + expect(getRenderedOverlay(2)).to.be.displayed; + expect(getRenderedOverlay(3)).to.not.be.displayed; + }); + + it('keeps backdrop status when used in combination with blocking', async () => { + const ctrl0 = overlays.add( + new GlobalOverlayController({ + isBlocking: false, + hasBackdrop: true, + contentTemplate: () => html` +

Content0

+ `, + }), + ); + await ctrl0.show(); + + const ctrl1 = overlays.add( + new GlobalOverlayController({ + isBlocking: false, + hasBackdrop: true, + contentTemplate: () => html` +

Content1

+ `, + }), + ); + await ctrl1.show(); + await ctrl1.hide(); + expect(ctrl0.hasActiveBackdrop).to.be.true; + expect(ctrl1.hasActiveBackdrop).to.be.false; + + await ctrl1.show(); + expect(ctrl0.hasActiveBackdrop).to.be.true; + expect(ctrl1.hasActiveBackdrop).to.be.true; + }); + }); + + describe('trapsKeyboardFocus (for a11y)', () => { + it('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + trapsKeyboardFocus: true, + contentTemplate: () => html` +

Content

+ `, + }), + ); + + const sibling1 = document.createElement('div'); + const sibling2 = document.createElement('div'); + document.body.insertBefore(sibling1, getRootNode()); + document.body.appendChild(sibling2); + + await ctrl.show(); + + [sibling1, sibling2].forEach(sibling => { + expect(sibling).to.have.attribute('aria-hidden', 'true'); + expect(sibling).to.have.attribute('inert'); + }); + expect(getRenderedOverlay(0).hasAttribute('aria-hidden')).to.be.false; + expect(getRenderedOverlay(0).hasAttribute('inert')).to.be.false; + + await ctrl.hide(); + + [sibling1, sibling2].forEach(sibling => { + expect(sibling).to.not.have.attribute('aria-hidden'); + expect(sibling).to.not.have.attribute('inert'); + }); + + // cleanup + document.body.removeChild(sibling1); + document.body.removeChild(sibling2); + }); + + /** + * style.userSelect: + * - chrome: 'none' + * - rest: undefined + * + * style.pointerEvents: + * - chrome: auto + * - IE11: visiblePainted + */ + it('disables pointer events and selection on inert elements', async () => { + const ctrl = overlays.add( + new GlobalOverlayController({ + trapsKeyboardFocus: true, + contentTemplate: () => html` +

Content

+ `, + }), + ); + + // show+hide are needed to create a root node + await ctrl.show(); + await ctrl.hide(); + + const sibling1 = document.createElement('div'); + const sibling2 = document.createElement('div'); + document.body.insertBefore(sibling1, getRootNode()); + document.body.appendChild(sibling2); + + await ctrl.show(); + + [sibling1, sibling2].forEach(sibling => { + expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['none', undefined]); + expect(window.getComputedStyle(sibling).pointerEvents).to.equal('none'); + }); + expect(window.getComputedStyle(getRenderedOverlay(0)).userSelect).to.be.oneOf([ + 'auto', + undefined, + ]); + expect(window.getComputedStyle(getRenderedOverlay(0)).pointerEvents).to.be.oneOf([ + 'auto', + 'visiblePainted', + ]); + + await ctrl.hide(); + + [sibling1, sibling2].forEach(sibling => { + expect(window.getComputedStyle(sibling).userSelect).to.be.oneOf(['auto', undefined]); + expect(window.getComputedStyle(sibling).pointerEvents).to.be.oneOf([ + 'auto', + 'visiblePainted', + ]); + }); + + // cleanup + document.body.removeChild(sibling1); + document.body.removeChild(sibling2); + }); + + it('keeps focus within overlay with multiple overlays with all traps on true', async () => { + const ctrl0 = overlays.add( + new GlobalOverlayController({ + trapsKeyboardFocus: true, + contentTemplate: () => html` +
+ Link0 +
+ `, + }), + ); + + const ctrl1 = overlays.add( + new GlobalOverlayController({ + trapsKeyboardFocus: true, + contentTemplate: () => html` +
+ Link1 +
+ `, + }), + ); + + await ctrl0.show(); + await ctrl1.show(); + expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.false; + expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.true; + + await ctrl1.hide(); + expect(ctrl0.hasActiveTrapsKeyboardFocus).to.be.true; + expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.false; + }); + }); +}); diff --git a/packages/overlays/test/ModalDialogController.test.js b/packages/overlays/test/ModalDialogController.test.js index ad3574e3c..1a01c3bd3 100644 --- a/packages/overlays/test/ModalDialogController.test.js +++ b/packages/overlays/test/ModalDialogController.test.js @@ -1,19 +1,32 @@ -import { expect } from '@open-wc/testing'; +import { expect, html } from '@open-wc/testing'; import { GlobalOverlayController } from '../src/GlobalOverlayController.js'; import { ModalDialogController } from '../src/ModalDialogController.js'; describe('ModalDialogController', () => { + let defaultOptions; + + before(() => { + defaultOptions = { + contentTemplate: () => html` +

my content

+ `, + }; + }); + it('extends GlobalOverlayController', () => { - expect(new ModalDialogController()).to.be.instanceof(GlobalOverlayController); + expect(new ModalDialogController(defaultOptions)).to.be.instanceof(GlobalOverlayController); }); it('has correct defaults', () => { - const controller = new ModalDialogController(); - expect(controller.hasBackdrop).to.equal(true); - expect(controller.isBlocking).to.equal(false); - expect(controller.preventsScroll).to.equal(true); - expect(controller.trapsKeyboardFocus).to.equal(true); - expect(controller.hidesOnEsc).to.equal(true); + const ctrl = new ModalDialogController(defaultOptions); + expect(ctrl.hasBackdrop).to.be.true; + expect(ctrl.isBlocking).to.be.false; + expect(ctrl.preventsScroll).to.be.true; + expect(ctrl.trapsKeyboardFocus).to.be.true; + expect(ctrl.hidesOnEsc).to.be.true; + expect(ctrl.overlayContainerPlacementClass).to.equal( + 'global-overlays__overlay-container--center', + ); }); }); diff --git a/packages/overlays/test/OverlaysManager.test.js b/packages/overlays/test/OverlaysManager.test.js index 404467b25..db471accf 100644 --- a/packages/overlays/test/OverlaysManager.test.js +++ b/packages/overlays/test/OverlaysManager.test.js @@ -1,21 +1,107 @@ -import { expect } from '@open-wc/testing'; -import sinon from 'sinon'; +import { expect, html } from '@open-wc/testing'; import { OverlaysManager } from '../src/OverlaysManager.js'; - -function createGlobalOverlayControllerMock() { - return { - sync: sinon.spy(), - update: sinon.spy(), - show: sinon.spy(), - hide: sinon.spy(), - }; -} +import { BaseOverlayController } from '../src/BaseOverlayController.js'; describe('OverlaysManager', () => { + let defaultOptions; + let mngr; + + before(() => { + defaultOptions = { + contentTemplate: () => html` +

my content

+ `, + }; + }); + + beforeEach(() => { + mngr = new OverlaysManager(); + }); + + afterEach(() => { + mngr.teardown(); + }); + + it('provides .globalRootNode as a render target on first access', () => { + expect(document.body.querySelectorAll('.global-overlays').length).to.equal(0); + const rootNode = mngr.globalRootNode; + expect(document.body.querySelector('.global-overlays')).to.equal(rootNode); + }); + + it('provides .teardown() for cleanup', () => { + const rootNode = mngr.globalRootNode; + expect(document.body.querySelector('.global-overlays')).to.equal(rootNode); + expect(document.head.querySelector('[data-global-overlays=""]')).not.be.undefined; + + mngr.teardown(); + expect(document.body.querySelectorAll('.global-overlays').length).to.equal(0); + expect(document.head.querySelector('[data-global-overlays=""]')).be.null; + + // safety check via private access (do not use this) + expect(mngr.constructor.__globalRootNode).to.be.undefined; + expect(mngr.constructor.__globalStyleNode).to.be.undefined; + }); + it('returns the newly added overlay', () => { - const myOverlays = new OverlaysManager(); - const myController = createGlobalOverlayControllerMock(); - expect(myOverlays.add(myController)).to.equal(myController); + const myController = new BaseOverlayController(defaultOptions); + expect(mngr.add(myController)).to.equal(myController); + }); + + it('can add/remove controllers', () => { + const dialog = new BaseOverlayController(defaultOptions); + const popup = new BaseOverlayController(defaultOptions); + mngr.add(dialog); + mngr.add(popup); + + expect(mngr.list).to.deep.equal([dialog, popup]); + + mngr.remove(popup); + expect(mngr.list).to.deep.equal([dialog]); + + mngr.remove(dialog); + expect(mngr.list).to.deep.equal([]); + }); + + it('throws if you try to add the same controller', () => { + const ctrl = new BaseOverlayController(defaultOptions); + mngr.add(ctrl); + expect(() => mngr.add(ctrl)).to.throw('controller instance is already added'); + }); + + it('throws if you try to remove a non existing controller', () => { + const ctrl = new BaseOverlayController(defaultOptions); + expect(() => mngr.remove(ctrl)).to.throw('could not find controller to remove'); + }); + + it('adds a reference to the manager to the controller', () => { + const dialog = new BaseOverlayController(defaultOptions); + mngr.add(dialog); + + expect(dialog.manager).to.equal(mngr); + }); + + it('has a .shownList which is ordered based on last shown', async () => { + const dialog = new BaseOverlayController(defaultOptions); + const dialog2 = new BaseOverlayController(defaultOptions); + mngr.add(dialog); + mngr.add(dialog2); + + expect(mngr.shownList).to.deep.equal([]); + + await dialog.show(); + expect(mngr.shownList).to.deep.equal([dialog]); + + await dialog2.show(); + expect(mngr.shownList).to.deep.equal([dialog2, dialog]); + + await dialog.show(); + expect(mngr.shownList).to.deep.equal([dialog, dialog2]); + + await dialog.hide(); + expect(mngr.shownList).to.deep.equal([dialog2]); + + await dialog2.hide(); + expect(mngr.shownList).to.deep.equal([]); }); }); diff --git a/packages/overlays/test/utils-tests/contain-focus.test.js b/packages/overlays/test/utils-tests/contain-focus.test.js index 8ba0b1259..679d03da7 100644 --- a/packages/overlays/test/utils-tests/contain-focus.test.js +++ b/packages/overlays/test/utils-tests/contain-focus.test.js @@ -53,7 +53,7 @@ const lightDomAutofocusTemplate = ` `; describe('containFocus()', () => { - it('starts focus at the root element when there is no element with autofocus', async () => { + it('starts focus at the root element when there is no element with [autofocus]', async () => { await fixture(lightDomTemplate); const root = document.getElementById('rootElement'); containFocus(root); @@ -63,7 +63,7 @@ describe('containFocus()', () => { expect(root.style.getPropertyValue('outline-style')).to.equal('none'); }); - it('starts focus at the element with the autofocus attribute', async () => { + it('starts focus at the element with [autofocus] attribute', async () => { await fixture(lightDomAutofocusTemplate); const el = document.querySelector('input[autofocus]'); containFocus(el); diff --git a/packages/popup/package.json b/packages/popup/package.json index b5f12a356..926166d58 100644 --- a/packages/popup/package.json +++ b/packages/popup/package.json @@ -39,6 +39,6 @@ "@lion/button": "^0.3.16", "@lion/icon": "^0.2.6", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/radio-group/package.json b/packages/radio-group/package.json index 313008c9b..945e414f1 100644 --- a/packages/radio-group/package.json +++ b/packages/radio-group/package.json @@ -39,6 +39,6 @@ "@lion/form": "^0.1.58", "@lion/radio": "^0.1.53", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/radio/package.json b/packages/radio/package.json index 2a11e7ff1..07511006f 100644 --- a/packages/radio/package.json +++ b/packages/radio/package.json @@ -38,6 +38,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/select-rich/package.json b/packages/select-rich/package.json index c030c4aa3..2c5f0f3d7 100644 --- a/packages/select-rich/package.json +++ b/packages/select-rich/package.json @@ -46,6 +46,6 @@ "devDependencies": { "@lion/form": "^0.1.58", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/select-rich/test/lion-select-rich-interaction.test.js b/packages/select-rich/test/lion-select-rich-interaction.test.js index 29abb7b37..c39026e76 100644 --- a/packages/select-rich/test/lion-select-rich-interaction.test.js +++ b/packages/select-rich/test/lion-select-rich-interaction.test.js @@ -280,7 +280,9 @@ describe('lion-select-rich interactions', () => { describe('Disabled', () => { it('cannot be focused if disabled', async () => { const el = await fixture(html` - + + + `); expect(el._invokerNode.tabIndex).to.equal(-1); }); @@ -312,7 +314,9 @@ describe('lion-select-rich interactions', () => { it('cannot be opened via click if disabled', async () => { const el = await fixture(html` - + + + `); el._invokerNode.click(); expect(el.opened).to.be.false; @@ -320,7 +324,9 @@ describe('lion-select-rich interactions', () => { it('reflects disabled attribute to invoker', async () => { const el = await fixture(html` - + + + `); expect(el._invokerNode.hasAttribute('disabled')).to.be.true; el.removeAttribute('disabled'); diff --git a/packages/select-rich/test/lion-select-rich.test.js b/packages/select-rich/test/lion-select-rich.test.js index d1c70d369..072150b0e 100644 --- a/packages/select-rich/test/lion-select-rich.test.js +++ b/packages/select-rich/test/lion-select-rich.test.js @@ -8,7 +8,9 @@ import '../lion-select-rich.js'; describe('lion-select-rich', () => { it('does not have a tabindex', async () => { const el = await fixture(html` - + + + `); expect(el.hasAttribute('tabindex')).to.be.false; }); @@ -16,7 +18,9 @@ describe('lion-select-rich', () => { describe('Invoker', () => { it('generates an lion-select-invoker if no invoker is provided', async () => { const el = await fixture(html` - + + + `); expect(el._invokerNode).to.exist; @@ -43,7 +47,9 @@ describe('lion-select-rich', () => { describe('overlay', () => { it('should be closed by default', async () => { const el = await fixture(html` - + + + `); expect(el.opened).to.be.false; }); @@ -117,7 +123,9 @@ describe('lion-select-rich', () => { describe('interaction-mode', () => { it('allows to specify an interaction-mode which determines other behaviors', async () => { const el = await fixture(html` - + + + `); expect(el.interactionMode).to.equal('mac'); }); @@ -279,6 +287,8 @@ describe('lion-select-rich', () => { expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('false'); el.opened = true; await el.updateComplete; + await el.updateComplete; // need 2 awaits as overlay.show is an async function + expect(el._invokerNode.getAttribute('aria-expanded')).to.equal('true'); }); }); diff --git a/packages/select/package.json b/packages/select/package.json index ccdec0cb1..eeba6a65d 100644 --- a/packages/select/package.json +++ b/packages/select/package.json @@ -37,6 +37,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/steps/package.json b/packages/steps/package.json index 999ce78bf..98527d1ed 100644 --- a/packages/steps/package.json +++ b/packages/steps/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/packages/textarea/package.json b/packages/textarea/package.json index 6d4a9cee7..c6f1cc0c6 100644 --- a/packages/textarea/package.json +++ b/packages/textarea/package.json @@ -38,6 +38,6 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/tooltip/package.json b/packages/tooltip/package.json index 611dec4d3..455594ba3 100644 --- a/packages/tooltip/package.json +++ b/packages/tooltip/package.json @@ -40,6 +40,6 @@ "@lion/button": "^0.3.16", "@lion/icon": "^0.2.6", "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6" + "@open-wc/testing": "^2.3.4" } } diff --git a/packages/tooltip/src/LionTooltip.js b/packages/tooltip/src/LionTooltip.js index 598efba08..05cf948bd 100644 --- a/packages/tooltip/src/LionTooltip.js +++ b/packages/tooltip/src/LionTooltip.js @@ -1,5 +1,4 @@ import { LionPopup } from '@lion/popup'; -import { overlays, LocalOverlayController } from '@lion/overlays'; export class LionTooltip extends LionPopup { constructor() { @@ -10,20 +9,8 @@ export class LionTooltip extends LionPopup { connectedCallback() { super.connectedCallback(); - this.contentNode = this.querySelector('[slot="content"]'); - this.invokerNode = this.querySelector('[slot="invoker"]'); this.contentNode.setAttribute('role', 'tooltip'); - this._controller = overlays.add( - new LocalOverlayController({ - hidesOnEsc: true, - hidesOnOutsideClick: true, - popperConfig: this.popperConfig, - contentNode: this.contentNode, - invokerNode: this.invokerNode, - }), - ); - this.__resetActive = () => { this.mouseActive = false; this.keyActive = false; diff --git a/packages/validate/package.json b/packages/validate/package.json index f9d23f0df..1de1a32a9 100644 --- a/packages/validate/package.json +++ b/packages/validate/package.json @@ -38,7 +38,7 @@ }, "devDependencies": { "@open-wc/demoing-storybook": "^0.2.0", - "@open-wc/testing": "^2.0.6", + "@open-wc/testing": "^2.3.4", "sinon": "^7.2.2" } } diff --git a/yarn.lock b/yarn.lock index 7a2ddd422..b1b7a4fa0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -777,11 +777,6 @@ resolved "https://registry.yarnpkg.com/@bundled-es-modules/axios/-/axios-0.18.1.tgz#8beedbc92e9b0ed7df7c6cbdc6dfce84d306d80b" integrity sha512-7c389uGe0dmfdedi9PQ3Om4vKg1HFzm/IntaqZ4FbXOo+gNiiPIM4He8MIkuRpgqUitbm1km0jOQ8p+tSpUp4Q== -"@bundled-es-modules/chai@^4.2.0": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@bundled-es-modules/chai/-/chai-4.2.2.tgz#88dffebd7cd1e87397738107c3c54b9b4ea56e5e" - integrity sha512-iGmVYw2/zJCoqyKTtWEYCtFmMyi8WmACQKtky0lpNyEKWX0YIOpKWGD7saMXL+tPpllss0otilxV0SLwyi3Ytg== - "@bundled-es-modules/fetch-mock@^6.5.2": version "6.5.2" resolved "https://registry.yarnpkg.com/@bundled-es-modules/fetch-mock/-/fetch-mock-6.5.2.tgz#f68d78dba49ffcb5b58bede5974c8a9dd035a6fb" @@ -1909,10 +1904,10 @@ universal-user-agent "^3.0.0" url-template "^2.0.8" -"@open-wc/building-utils@^2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@open-wc/building-utils/-/building-utils-2.8.0.tgz#e3c11ac844d2a150136fd41017dbeed263217638" - integrity sha512-Kf1T6b29ETi5r5wzrQNcNnQQuYmRd2YrKXk54YuTm2nErFbYdXWlPoBQrDjBtlo0STOgoY59HOUpWRbGVIbI2g== +"@open-wc/building-utils@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@open-wc/building-utils/-/building-utils-2.8.2.tgz#0ccc3302add7d2e2cbd5b8328764369e506b50b8" + integrity sha512-DAJbvt1A2ACkUqpR/cVE7bTb89o0fEf6MTlpmmd869Zkn6+x+9qR02T+E/xPw2awFv2MhMihHlZWTaZgQ9Xs+w== dependencies: "@babel/core" "^7.3.3" "@babel/plugin-syntax-dynamic-import" "^7.2.0" @@ -1985,15 +1980,16 @@ eslint-plugin-import "^2.18.0" eslint-plugin-wc "^1.2.0" -"@open-wc/karma-esm@^2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@open-wc/karma-esm/-/karma-esm-2.4.1.tgz#bc241240f0b39baa1d55b132bc6df76a430fc8b8" - integrity sha512-sL/Jf8krPqPmSuTyqdjTtR7IHmChPsk5sU/6BSW6XYYrHQv0ubDyRHRcpfQpUp3zfi35mWiznb7CJKrcq+nQKg== +"@open-wc/karma-esm@^2.5.8": + version "2.5.8" + resolved "https://registry.yarnpkg.com/@open-wc/karma-esm/-/karma-esm-2.5.8.tgz#ed7e6739bd07a658324fa92bd304eaef88e78ea6" + integrity sha512-4FtYOAdzQGvDnVJ27Xvfm6y1ZG1VybF84Y9tWCTJhkNFQgD5eY9DY3Ofw+2vmVEQYH0vKVm4hj2zk6cNjEpaUQ== dependencies: - "@open-wc/building-utils" "^2.8.0" + "@open-wc/building-utils" "^2.8.2" babel-plugin-istanbul "^5.1.4" + chokidar "^3.0.2" deepmerge "^3.3.0" - es-dev-server "^1.14.0" + es-dev-server "^1.18.0" minimatch "^3.0.4" portfinder "^1.0.21" request "^2.88.0" @@ -2006,36 +2002,36 @@ eslint-config-prettier "^3.3.0" prettier "^1.15.0" -"@open-wc/semantic-dom-diff@^0.13.16", "@open-wc/semantic-dom-diff@^0.13.21": +"@open-wc/semantic-dom-diff@^0.13.16": version "0.13.21" resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.13.21.tgz#718b9ec5f9a98935fc775e577ad094ae8d8b7dea" integrity sha512-BONpjHcGX2zFa9mfnwBCLEmlDsOHzT+j6Qt1yfK3MzFXFtAykfzFjAgaxPetu0YbBlCfXuMlfxI4vlRGCGMvFg== -"@open-wc/semantic-dom-diff@^0.14.1": - version "0.14.1" - resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.14.1.tgz#c1362fac09a46390584a857387024137687bd39a" - integrity sha512-VmrJn8MloBnLx0LmKKASKBL7Y/hj3ci4tugWcRGcm3VE7SdUF7bZz9g4VkHaus18cfLQonziwRouw2Insx7I7w== +"@open-wc/semantic-dom-diff@^0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.14.2.tgz#4ad2a6fedc22992c6048729fbf7104e472f590d3" + integrity sha512-+ENGbkgoruTtuNGUVLi9hCC6+IJVBM/lnFDGjKBUt7T6RyAgidBTV+GPMzpQtfHhLRasVJmcjRW8D2him8HvAA== -"@open-wc/testing-helpers@^1.1.7", "@open-wc/testing-helpers@^1.2.1": +"@open-wc/testing-helpers@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@open-wc/testing-helpers/-/testing-helpers-1.2.1.tgz#eecba5ccfe808f9667caf149e68cd80d781f28e0" integrity sha512-FZBjqM81GQc+Q8W4YdWNwwk64+PW6frCvHyeov5ivCR2K7SJXwTPRcQIi0qU/6VVRVDlQeQ39PH8oSnjnIYpvQ== -"@open-wc/testing-karma-bs@^1.1.47": - version "1.1.47" - resolved "https://registry.yarnpkg.com/@open-wc/testing-karma-bs/-/testing-karma-bs-1.1.47.tgz#8e8ceab7e9fea751703c7aedea22a45889642d7b" - integrity sha512-TcINS7scTBO3c7FkXySrZbmOqY/+XgaJ0EV4ns4YchHpSl5B48Xvdx6VThMx2Xf2/yMMz/GB2sjH8UO3tX0k5A== +"@open-wc/testing-karma-bs@^1.1.58": + version "1.1.58" + resolved "https://registry.yarnpkg.com/@open-wc/testing-karma-bs/-/testing-karma-bs-1.1.58.tgz#d2c46d0cedc2927bc69689a0c25ef906e7505ba5" + integrity sha512-zj8BbozAIm7HEOYly594rzvEiENjH06R7RM5fbdkwbTxvcC4m/TmvGpVmwXX37XgxqKo5WxIA7wXeQkReCA8xw== dependencies: - "@open-wc/testing-karma" "^3.1.22" + "@open-wc/testing-karma" "^3.1.33" "@types/node" "^11.13.0" karma-browserstack-launcher "^1.0.0" -"@open-wc/testing-karma@^3.1.22": - version "3.1.22" - resolved "https://registry.yarnpkg.com/@open-wc/testing-karma/-/testing-karma-3.1.22.tgz#6c0e14317880151622a8eab0f7ae166fd035a418" - integrity sha512-oDLCBqm8HbMnwok5/COVWmubW+UYDaWrokMOfmwmLq6eMlNnEkr3PNuKW0nII6RY5UkWaefbC4BuIWD/S6vt3w== +"@open-wc/testing-karma@^3.1.33": + version "3.1.33" + resolved "https://registry.yarnpkg.com/@open-wc/testing-karma/-/testing-karma-3.1.33.tgz#3e0ff02cc2db0cce01ac4a9fe1e979f94c82d79c" + integrity sha512-TzzS0CyN62ZxJ2SmDt9LIcT8lRb/jO+mv7U1Lg69iu0KLRJZTrFILstTEVHww0UHgrbOE8N+LbmdYiH8ryBIUQ== dependencies: - "@open-wc/karma-esm" "^2.4.1" + "@open-wc/karma-esm" "^2.5.8" axe-core "^3.3.1" istanbul-instrumenter-loader "^3.0.0" karma "^4.0.0" @@ -2057,27 +2053,13 @@ wallaby-webpack "^3.0.0" webpack "^4.28.0" -"@open-wc/testing@^2.0.6": - version "2.2.8" - resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-2.2.8.tgz#ed4d3621601512eeccb0a759127589776da1d279" - integrity sha512-z+eRA6luH7TR8iewZJnZBN1JOmDGViFOQqdMoHkQPvQ0C2aBZNW/IrdbYUa+YY4rC+5jtWXHMf9gYdxw/B16bg== - dependencies: - "@bundled-es-modules/chai" "^4.2.0" - "@open-wc/chai-dom-equals" "^0.12.36" - "@open-wc/semantic-dom-diff" "^0.13.21" - "@open-wc/testing-helpers" "^1.1.7" - "@types/chai" "^4.1.7" - "@types/mocha" "^5.0.0" - chai-a11y-axe "^1.1.1" - mocha "^5.0.0" - -"@open-wc/testing@^2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-2.3.2.tgz#3199d4049d2163d4e74b9390fafbbb8dc8e51eeb" - integrity sha512-fdFXO3TVib/3+67BLwMYPqQgs7FaUajoMzazjtFNSUxFZgvqdHNz8gMBUYyD49wdCxBb9RAUtjQTjaLXEY+uWQ== +"@open-wc/testing@^2.3.4": + version "2.3.4" + resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-2.3.4.tgz#cb29bbba14208c5ddfcb95119aff31ab1dfe2793" + integrity sha512-Jne5opdKgH1EvySXdMv5VO+c/+v1aL8EQQUqe7vggmrf9cEUdQ/fvLBP4qEeUF7kJdzVvvOjfEzPuA5Y/NEUmQ== dependencies: "@open-wc/chai-dom-equals" "^0.12.36" - "@open-wc/semantic-dom-diff" "^0.14.1" + "@open-wc/semantic-dom-diff" "^0.14.2" "@open-wc/testing-helpers" "^1.2.1" "@types/chai" "^4.1.7" "@types/chai-dom" "^0.0.8" @@ -4299,11 +4281,6 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chai-a11y-axe@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chai-a11y-axe/-/chai-a11y-axe-1.1.1.tgz#5e61e8c2d4b1a5b1b039658d8a21d147c18f612d" - integrity sha512-pLrVkN39GUSZl6Yd+HRJGo4XgXdwiSO2pt0NItb5vENRvh+0rTTCQK2ptBlMqtxOw/RCfFNi7avJjAEzGaa66A== - chai-a11y-axe@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/chai-a11y-axe/-/chai-a11y-axe-1.2.1.tgz#173c9bb5cd534ccc9039edc3cdeed7692ab60f15" @@ -4420,7 +4397,7 @@ chokidar@^2.0.2: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.0.0: +chokidar@^3.0.0, chokidar@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681" integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA== @@ -5896,15 +5873,15 @@ es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13 is-regex "^1.0.4" object-keys "^1.0.12" -es-dev-server@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/es-dev-server/-/es-dev-server-1.14.0.tgz#165c332915c1494b0a518417720eaa4460822c56" - integrity sha512-6HhP4GlYyn9y7ptPU8Yset+dFDdGiA/2pVLIu7W+wg+HWPIqKIrBenlxYoGvLfR4MzYryG320BAJNW/W4ofYkw== +es-dev-server@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/es-dev-server/-/es-dev-server-1.18.0.tgz#56dd48868c4fa1f2b5029886a35351e385830651" + integrity sha512-cw6NyLxim8WB5EeAJOJA4wEq87ZC3NTIx7earYVtR4k3ihJMuLm2pLFpMqovWao7zWZ1WQgoETNsdxSx+0uj3A== dependencies: "@babel/core" "^7.4.5" "@babel/plugin-syntax-import-meta" "^7.2.0" "@babel/preset-env" "^7.4.5" - "@open-wc/building-utils" "^2.8.0" + "@open-wc/building-utils" "^2.8.2" "@types/minimatch" "^3.0.3" babel-plugin-bare-import-rewrite "^1.5.1" camelcase "^5.3.1" @@ -5913,7 +5890,7 @@ es-dev-server@^1.14.0: command-line-usage "^5.0.5" debounce "^1.2.0" deepmerge "^3.3.0" - es-module-lexer "^0.2.0" + es-module-lexer "0.3.9" get-stream "^5.1.0" is-stream "^2.0.0" koa "^2.7.0" @@ -5925,12 +5902,13 @@ es-dev-server@^1.14.0: path-is-inside "^1.0.2" portfinder "^1.0.21" resolve "^1.12.0" + strip-ansi "^5.2.0" whatwg-url "^7.0.0" -es-module-lexer@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.2.0.tgz#b0e88d22dc270dc5f8c2d426e63a42252188eee9" - integrity sha512-HAcAIT+A5ugnSgC1pS+tgVWjlaIx8HZo8fI7xa90WdAjrJqXEZO0oszJQV3q04wCD4/ZQDlgu8nOLPxMWlaQ8g== +es-module-lexer@0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.9.tgz#fbdbb35e1ad434fc71f5d83a887781fadbaca83a" + integrity sha512-beowXiBsaVS208GHoJckwOxlG/RpWLLz2ioJHwkUJ3RGFmW508E5RfntV21bMjzO3hQcwZOTHewUFwnGzrBtIg== es-module-shims@^0.2.13: version "0.2.15"