diff --git a/packages/overlays/src/DynamicOverlayController.js b/packages/overlays/src/DynamicOverlayController.js index f7c8f70d6..02110b907 100644 --- a/packages/overlays/src/DynamicOverlayController.js +++ b/packages/overlays/src/DynamicOverlayController.js @@ -34,6 +34,8 @@ export class DynamicOverlayController { if (!this.content) { this.content = document.createElement('div'); } + this.__fakeExtendsEventTarget(); + this.__delegateEvent = this.__delegateEvent.bind(this); } add(ctrlToAdd) { @@ -71,9 +73,13 @@ export class DynamicOverlayController { if (this.isShown === true) { throw new Error('You can not switch overlays while being shown'); } + const prevActive = this.active; + this.active.switchOut(); ctrlToSwitchTo.switchIn(); this.__active = ctrlToSwitchTo; + + this._delegateEvents(this.__active, prevActive); } async show() { @@ -99,4 +105,24 @@ export class DynamicOverlayController { get invokerNode() { return this.active.invokerNode; } + + _delegateEvents(active, prevActive) { + ['show', 'hide'].forEach(event => { + active.addEventListener(event, this.__delegateEvent); + prevActive.removeEventListener(event, this.__delegateEvent); + }); + } + + __delegateEvent(ev) { + ev.stopPropagation(); + this.dispatchEvent(new Event(ev.type)); + } + + // TODO: this method has to be removed when EventTarget polyfill is available on IE11 + __fakeExtendsEventTarget() { + const delegate = document.createDocumentFragment(); + ['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => { + this[funcName] = (...args) => delegate[funcName](...args); + }); + } } diff --git a/packages/overlays/test/DynamicOverlayController.test.js b/packages/overlays/test/DynamicOverlayController.test.js index 07a8daf4a..aacc904ec 100644 --- a/packages/overlays/test/DynamicOverlayController.test.js +++ b/packages/overlays/test/DynamicOverlayController.test.js @@ -133,4 +133,34 @@ describe('DynamicOverlayController', () => { expect(globalOutSpy).to.have.callCount(1); expect(localInSpy).to.have.callCount(1); }); + + describe('API abstraction for active overlay controller', () => { + describe('Events', () => { + it('delegates "show/hide" event', async () => { + const ctrl = new DynamicOverlayController(); + const global = new FakeGlobalCtrl(defaultOptions); + const local = new FakeLocalCtrl(defaultOptions); + ctrl.add(global); + ctrl.add(local); + ctrl.switchTo(local); + + const showSpy = sinon.spy(); + const hideSpy = sinon.spy(); + + ctrl.addEventListener('show', showSpy); + ctrl.addEventListener('hide', hideSpy); + + await ctrl.show(); + expect(showSpy.callCount).to.equal(1); + await ctrl.hide(); + expect(hideSpy.callCount).to.equal(1); + + ctrl.switchTo(global); + await ctrl.show(); + expect(showSpy.callCount).to.equal(2); + await ctrl.hide(); + expect(hideSpy.callCount).to.equal(2); + }); + }); + }); }); diff --git a/packages/select-rich/src/LionSelectRich.js b/packages/select-rich/src/LionSelectRich.js index 34302ee67..0e1a70db1 100644 --- a/packages/select-rich/src/LionSelectRich.js +++ b/packages/select-rich/src/LionSelectRich.js @@ -548,11 +548,15 @@ export class LionSelectRich extends FormRegistrarMixin( } } - __setupOverlay() { - this.__overlay = overlays.add( + /** + * @overridable Subclassers can override the default + */ + // eslint-disable-next-line class-methods-use-this + _defineOverlay({ invokerNode, contentNode } = {}) { + return overlays.add( new LocalOverlayController({ - contentNode: this._listboxNode, - invokerNode: this._invokerNode, + contentNode, + invokerNode, hidesOnEsc: false, hidesOnOutsideClick: true, inheritsReferenceObjectWidth: true, @@ -566,6 +570,13 @@ export class LionSelectRich extends FormRegistrarMixin( }, }), ); + } + + __setupOverlay() { + this.__overlay = this._defineOverlay({ + invokerNode: this._invokerNode, + contentNode: this._listboxNode, + }); this.__overlayOnShow = () => { this.opened = true; diff --git a/packages/select-rich/test/lion-select-rich.test.js b/packages/select-rich/test/lion-select-rich.test.js index 072150b0e..4015f05e3 100644 --- a/packages/select-rich/test/lion-select-rich.test.js +++ b/packages/select-rich/test/lion-select-rich.test.js @@ -1,9 +1,16 @@ -import { expect, fixture, html, aTimeout } from '@open-wc/testing'; -import './keyboardEventShimIE.js'; - +import { expect, fixture, html, aTimeout, defineCE, unsafeStatic } from '@open-wc/testing'; import '@lion/option/lion-option.js'; +import { + overlays, + LocalOverlayController, + GlobalOverlayController, + DynamicOverlayController, +} from '@lion/overlays'; + +import './keyboardEventShimIE.js'; import '../lion-options.js'; import '../lion-select-rich.js'; +import { LionSelectRich } from '../index.js'; describe('lion-select-rich', () => { it('does not have a tabindex', async () => { @@ -328,4 +335,51 @@ describe('lion-select-rich', () => { }); }); }); + + describe('Subclassers', () => { + it('allows to override the type of overlays', async () => { + const mySelectTagString = defineCE( + class MySelect extends LionSelectRich { + _defineOverlay({ invokerNode, contentNode }) { + // add a DynamicOverlayController + const dynamicCtrl = new DynamicOverlayController(); + + const localCtrl = overlays.add( + new LocalOverlayController({ + contentNode, + invokerNode, + }), + ); + dynamicCtrl.add(localCtrl); + + const globalCtrl = overlays.add( + new GlobalOverlayController({ + contentNode, + invokerNode, + }), + ); + dynamicCtrl.add(globalCtrl); + + return dynamicCtrl; + } + }, + ); + + const mySelectTag = unsafeStatic(mySelectTagString); + + const el = await fixture(html` + <${mySelectTag} label="Favorite color" name="color"> + + ${Array(2).map( + (_, i) => html` + value ${i} + `, + )} + + + `); + + expect(el.__overlay).to.be.instanceOf(DynamicOverlayController); + }); + }); });