fix: no longer use overlay templates

This commit is contained in:
Thomas Allmer 2019-11-30 14:59:00 +01:00 committed by Thomas Allmer
parent c899cf26d2
commit 49974bd2b8
24 changed files with 334 additions and 467 deletions

View file

@ -34,8 +34,6 @@
"@lion/overlays": "^0.6.4"
},
"devDependencies": {
"@lion/button": "^0.3.43",
"@lion/icon": "^0.2.9",
"@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4"
}

View file

@ -15,21 +15,18 @@ export class LionDialog extends OverlayMixin(LitElement) {
}
_setupOpenCloseListeners() {
this.__close = () => {
this.opened = false;
};
this.__toggle = () => {
this.opened = !this.opened;
};
if (this._overlayCtrl.invokerNode) {
this._overlayCtrl.invokerNode.addEventListener('click', this.__toggle);
if (this._overlayInvokerNode) {
this._overlayInvokerNode.addEventListener('click', this.__toggle);
}
}
_teardownOpenCloseListeners() {
if (this._overlayCtrl.invokerNode) {
this._overlayCtrl.invokerNode.removeEventListener('click', this.__toggle);
if (this._overlayInvokerNode) {
this._overlayInvokerNode.removeEventListener('click', this.__toggle);
}
}

View file

@ -1,7 +1,5 @@
import { storiesOf, html, withKnobs, object } from '@open-wc/demoing-storybook';
import { css } from '@lion/core';
import '@lion/icon/lion-icon.js';
import '@lion/button/lion-button.js';
import '../lion-dialog.js';
const dialogDemoStyle = css`
@ -67,24 +65,25 @@ storiesOf('Overlays Specific WC | Dialog', module)
</p>
<p>
To close your dialog from some action performed inside the content slot, fire a
<code>close</code> event.
<code>hide</code> event.
</p>
<p>
For the dialog to close, it will need to bubble to the content slot (use
<code>bubbles: true</code>. Also <code>composed: true</code> if it needs to traverse shadow
boundaries)
<code>bubbles: true</code>. If absolutely needed <code>composed: true</code> can be used to
traverse shadow boundaries)
</p>
<p>The demo below demonstrates this</p>
<div class="demo-box">
<lion-dialog>
<lion-button slot="invoker">Dialog</lion-button>
<button slot="invoker">Dialog</button>
<div slot="content" class="dialog">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-dialog>
</div>
@ -93,14 +92,15 @@ storiesOf('Overlays Specific WC | Dialog', module)
.add('Custom configuration', () => {
const dialog = placement => html`
<lion-dialog .config=${{ viewportConfig: { placement } }}>
<lion-button slot="invoker">Dialog ${placement}</lion-button>
<button slot="invoker">Dialog ${placement}</button>
<div slot="content" class="dialog">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-dialog>
`;
@ -118,14 +118,15 @@ storiesOf('Overlays Specific WC | Dialog', module)
.add('Toggle placement with knobs', () => {
const dialog = html`
<lion-dialog .config=${object('config', { viewportConfig: { placement: 'center' } })}>
<lion-button slot="invoker">Dialog</lion-button>
<button slot="invoker">Dialog</button>
<div slot="content" class="dialog">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-dialog>
`;

View file

@ -1,77 +1,34 @@
import { expect, fixture, html } from '@open-wc/testing';
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import { runOverlayMixinSuite } from '@lion/overlays/test-suites/OverlayMixin.suite.js';
import '../lion-dialog.js';
// Smoke tests dialog
describe('lion-dialog', () => {
describe('Basic', () => {
it('should not be shown by default', async () => {
const el = await fixture(html`
<lion-dialog>
<div slot="content" class="dialog">Hey there</div>
<lion-button slot="invoker">Popup button</lion-button>
</lion-dialog>
`);
expect(el._overlayCtrl.isShown).to.be.false;
});
describe('Integration tests', () => {
const tagString = 'lion-dialog';
const tag = unsafeStatic(tagString);
runOverlayMixinSuite({
tagString,
tag,
suffix: ' for lion-dialog',
});
});
describe('Basic', () => {
it('should show content on invoker click', async () => {
const el = await fixture(html`
<lion-dialog>
<div slot="content" class="dialog">
Hey there
</div>
<lion-button slot="invoker">Popup button</lion-button>
<button slot="invoker">Popup button</button>
</lion-dialog>
`);
const invoker = el.querySelector('[slot="invoker"]');
invoker.click();
expect(el._overlayCtrl.isShown).to.be.true;
});
it('should hide content on close event', async () => {
const el = await fixture(html`
<lion-dialog>
<div slot="content" class="dialog">
Hey there
<button
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}
>
x
</button>
</div>
<lion-button slot="invoker">Popup button</lion-button>
</lion-dialog>
`);
const invoker = el.querySelector('[slot="invoker"]');
invoker.click();
expect(el._overlayCtrl.isShown).to.be.true;
const closeBtn = el._overlayCtrl.contentNode.querySelector('button');
closeBtn.click();
expect(el._overlayCtrl.isShown).to.be.false;
});
it('should respond to initially and dynamically setting the config', async () => {
const el = await fixture(html`
<lion-dialog .config=${{ trapsKeyboardFocus: false, viewportConfig: { placement: 'top' } }}>
<div slot="content" class="dialog">Hey there</div>
<lion-button slot="invoker">Popup button</lion-button>
</lion-dialog>
`);
await el._overlayCtrl.show();
expect(el._overlayCtrl.trapsKeyboardFocus).to.be.false;
el.config = { viewportConfig: { placement: 'left' } };
expect(el._overlayCtrl.viewportConfig.placement).to.equal('left');
expect(
el._overlayCtrl._contentNodeWrapper.classList.contains(
'global-overlays__overlay-container--left',
),
);
expect(el.opened).to.be.true;
});
});
});

View file

@ -90,14 +90,9 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
];
}
constructor() {
super();
this.__dispatchCloseEvent = this.__dispatchCloseEvent.bind(this);
}
__dispatchCloseEvent() {
__dispatchHideEvent() {
// Designed to work in conjunction with ModalDialogController
this.dispatchEvent(new CustomEvent('dialog-close'), { bubbles: true, composed: true });
this.dispatchEvent(new CustomEvent('hide'), { bubbles: true });
}
render() {
@ -109,7 +104,7 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
<slot name="heading"></slot>
</h1>
<button
@click="${this.__dispatchCloseEvent}"
@click="${this.__dispatchHideEvent}"
id="close-button"
title="${this.msgLit('lion-calendar-overlay-frame:close')}"
aria-label="${this.msgLit('lion-calendar-overlay-frame:close')}"

View file

@ -204,16 +204,23 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
* this is our source to give as .contentNode to OverlayController.
* Important: do not change the name of this method.
*/
// TODO: Refactor to new overlay system public API --> @close=${() => { this.opened = false; }}
_overlayTemplate() {
// TODO: add performance optimization to only render the calendar if needed
return html`
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}>
<lion-calendar-overlay-frame>
<span slot="heading">${this.calendarHeading}</span>
${this._calendarTemplate()}
</lion-calendar-overlay-frame>
`;
}
render() {
return html`
${this.labelTemplate()} ${this.helpTextTemplate()} ${this.inputGroupTemplate()}
${this.feedbackTemplate()} ${this._overlayTemplate()}
`;
}
/**
* Subclassers can replace this with their custom extension of
* LionCalendar, like `<my-calendar id="calendar"></my-calendar>`
@ -331,4 +338,11 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
get _overlayInvokerNode() {
return this._invokerElement;
}
/**
* @override Configures OverlayMixin
*/
get _overlayContentNode() {
return this.shadowRoot.querySelector('lion-calendar-overlay-frame');
}
}

View file

@ -2,8 +2,6 @@
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
> Note: Migrating from the old system (`overlays.add(new SomeController({...}))`)? Please check out our [migration guidelines](./docs/migration.md)
Supports different types of overlays like dialogs, toasts, tooltips, dropdown, etc.
Manages their position on the screen relative to other elements, including other overlays.
@ -16,7 +14,6 @@ Its purpose is to make it easy to use our Overlay System declaratively. It can b
- lion-overlay web component:
- Show content when clicking the invoker
- Respond to overlay-close event in the slot="content" element, to close the content
- Have a `.config` object to set or update the OverlayController's configuration
- [**OverlaysManager**](./docs/OverlaysManager.md), a global repository keeping track of all different types of overlays
@ -37,14 +34,14 @@ npm i --save @lion/overlays
import '@lion/overlays/lion-overlay.js';
html`
<lion-overlay.config=${{
<lion-overlay .config=${{
placementMode: 'global',
viewportConfig: { placement: 'bottom-right' },
}}>
<div slot="content">
This is an overlay
<button
@click=${e => e.target.dispatchEvent(new Event('overlay-close', { bubbles: true }))}
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>x</button>
<div>
<button slot="invoker">

View file

@ -32,6 +32,8 @@ All boolean flags default to 'false'.
```text
- {Boolean} trapsKeyboardFocus - rotates tab.
- {Boolean} hidesOnEsc - hides the overlay when pressing [esc].
- {Boolean} hidesOnHideEventInContentNode - (defaults to true) hides if an event called "hide" is fired within the content
- {Boolean} hidesOnOutsideClick - hides if user clicks outside of the overlay
- {Element} elementToFocusAfterHide - the element that should be called `.focus()` on after dialog closes.
- {Boolean} hasBackdrop - whether it should have a backdrop. (local mode only)
- {Boolean} isBlocking - hides other overlays when multiple are opened.

View file

@ -1,141 +0,0 @@
# Migration Guidelines Overlay System
If you are still using the old overlay system, we encourage you to migrate. The new way is more reliable, less error-prone and a lot easier to maintain. In addition, we now have a web component `lion-dialog` which is a declarative way of adding a modal dialog inside your template!
## Declaratively (encouraged)
Using generic `lion-overlay`:
```js
import { withBottomSheetConfig } from '@lion/overlays';
import '@lion/overlays/lion-overlay.js';
const template = html`
<lion-overlay
.config=${{
...withBottomSheetConfig(),
viewportConfig: { placement: 'top-right' },
}}
>
<button slot="invoker">Click me!</button>
<div slot="content">
<div>Hello, World!</div>
<button @click=${e => e.target.dispatchEvent(new Event('overlay-close', { bubbles: true }))}>
Close
</button>
</div>
</lion-overlay>
`;
```
Or using a more specific component like `lion-tooltip`, which toggles on-hover:
```js
import '@lion/tooltip/lion-tooltip.js';
const template = html`
<lion-tooltip .config=${{ popperConfig: { placement: 'top-right' } }}>
<button slot="invoker">Hover me!</button>
<div slot="content">
<div>Hello, World!</div>
</div>
</lion-tooltip>
`;
```
Or `lion-dialog` which uses modal dialog configuration defaults
```js
import '@lion/dialog/lion-dialog.js';
const template = html`
<lion-dialog .config=${{ viewportConfig: { placement: 'top-right' } }}>
<button slot="invoker">Click me!</button>
<div slot="content">
<div>Hello, World!</div>
<button @click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))}>
Close
</button>
</div>
</lion-dialog>
`;
```
## Instantiating an overlay controller (discouraged)
### Old
```js
import { overlays, GlobalOverlayController } from '@lion/overlays';
const ctrl = overlays.add(
new GlobalOverlayController({
contentTemplate: () => html`
<div>My content</div>
`,
}),
);
const template = html`
<lion-button @click="${event => ctrl.show(event.target)}">
Open dialog
</lion-button>
`;
```
### New
> Note: The OverlayController is render-system agnostic, you are responsible for passing a node (and rendering it prior).
> For lit-html, we will use a simple helper. Let us know if you think we should export this.
```js
import { render } from '@lion/core';
function renderOffline(litHtmlTemplate) {
const offlineRenderContainer = document.createElement('div');
render(litHtmlTemplate, offlineRenderContainer);
return offlineRenderContainer.firstElementChild;
}
```
This example shows how you can use our configuration generators.
```js
import { OverlayController, withModalDialogConfig } from '@lion/overlays';
const ctrl = new OverlayController({
...withModalDialogConfig(),
contentTemplate: renderOffline(html`
<div>My content</div>
`),
});
const template = html`
<lion-button @click="${event => ctrl.show(event.target)}">
Open dialog
</lion-button>
`;
```
### New (local example)
```js
import { OverlayController } from '@lion/overlays';
const ctrl = new OverlayController({
...withModalDialogConfig(),
placementMode: 'local',
hidesOnEsc: true,
hidesOnOutsideClick: true,
contentNode: renderOffline(html`
<div>United Kingdom</div>
`),
invokerNode: renderOffline(html`
<button @click=${() => ctrl.toggle()}>UK</button>
`),
});
const template = html`
<div>In the ${ctrl.invoker}${ctrl.content} the weather is nice.</div>
`;
```

View file

@ -29,6 +29,7 @@
"stories",
"test",
"test-helpers",
"test-suites",
"translations",
"*.js"
],
@ -37,8 +38,6 @@
"popper.js": "^1.15.0"
},
"devDependencies": {
"@lion/button": "^0.3.43",
"@lion/icon": "^0.2.9",
"@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4",
"@open-wc/testing-helpers": "^1.0.0",

View file

@ -33,6 +33,7 @@ export class OverlayController {
trapsKeyboardFocus: false,
hidesOnEsc: false,
hidesOnOutsideClick: false,
hidesOnHideEventInContentNode: true,
isTooltip: false,
handlesUserInteraction: false,
handlesAccessibility: false,
@ -336,6 +337,9 @@ export class OverlayController {
if (this.hidesOnOutsideClick) {
this._handleHidesOnOutsideClick({ phase });
}
if (this.hidesOnHideEventInContentNode) {
this._handleHidesOnHideEventInContentNode({ phase });
}
if (this.handlesAccessibility) {
this._handleAccessibility({ phase });
}
@ -483,10 +487,26 @@ export class OverlayController {
if (phase === 'show') {
this.__escKeyHandler = ev => ev.key === 'Escape' && this.hide();
this.contentNode.addEventListener('keyup', this.__escKeyHandler);
this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
if (this.invokerNode) {
this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
}
} else if (phase === 'hide') {
this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
if (this.invokerNode) {
this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
}
}
}
_handleHidesOnHideEventInContentNode({ phase }) {
if (phase === 'show') {
this.__hideEventInContentNodeHandler = ev => {
ev.stopPropagation();
this.hide();
};
this.contentNode.addEventListener('hide', this.__hideEventInContentNodeHandler);
} else if (phase === 'hide') {
this.contentNode.removeEventListener('keyup', this.__hideEventInContentNodeHandler);
}
}

View file

@ -1,4 +1,4 @@
import { render, dedupeMixin } from '@lion/core';
import { dedupeMixin } from '@lion/core';
import { OverlayController } from './OverlayController.js';
/**
@ -16,54 +16,26 @@ export const OverlayMixin = dedupeMixin(
type: Boolean,
reflect: true,
},
config: {
type: Object,
},
closeEventName: {
type: String,
},
};
}
constructor() {
super();
this.opened = false;
this.config = {};
this.closeEventName = 'overlay-close';
}
get opened() {
return this._overlayCtrl.isShown;
}
set opened(show) {
if (show) {
this.dispatchEvent(new Event('before-show'));
}
this._opened = show; // mainly captured for sync on connectedCallback
if (this._overlayCtrl) {
this.__syncOpened();
}
}
get config() {
return this._config;
return this.__config;
}
set config(value) {
if (this._overlayCtrl) {
this._overlayCtrl.updateConfig(value);
}
this._config = value;
this.__config = value;
}
/**
* @overridable method `_overlayTemplate`
* Be aware that the overlay will be placed in a different shadow root.
* Therefore, style encapsulation should be provided by the contents of
* _overlayTemplate
* @return {TemplateResult}
*/
/**
* @overridable method `_defineOverlay`
* @desc returns an instance of a (dynamic) overlay controller
@ -88,7 +60,19 @@ export const OverlayMixin = dedupeMixin(
*/
// eslint-disable-next-line
_defineOverlayConfig() {
return {};
return {
placementMode: 'local',
};
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('opened')) {
if (this._overlayCtrl) {
this.__syncToOverlayController();
}
}
}
/**
@ -106,44 +90,19 @@ export const OverlayMixin = dedupeMixin(
// eslint-disable-next-line class-methods-use-this
_teardownOpenCloseListeners() {}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
this._createOverlay();
// Default close event catcher on the contentNode which is useful if people want to close
// their overlay but the content is not in the global root node (nowhere near the overlay component)
this.__close = () => {
this.opened = false;
};
this._overlayCtrl.contentNode.addEventListener(this.closeEventName, this.__close);
this._setupOpenCloseListeners();
this.__syncOpened();
this.__syncPopper();
}
firstUpdated(c) {
super.firstUpdated(c);
this._createOutletForLocalOverlay();
}
updated(c) {
super.updated(c);
if (this.__managesOverlayViaTemplate) {
this._renderOverlayContent();
}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
// we setup in firstUpdated so we can use nodes from light and shadowDom
this._setupOverlayCtrl();
}
disconnectedCallback() {
if (super.disconnectedCallback) {
super.disconnectedCallback();
}
this.opened = false;
this._overlayCtrl.contentNode.removeEventListener(this.closeEventName, this.__close);
this._teardownOpenCloseListeners();
this._overlayCtrl.teardown();
if (this._overlayCtrl) {
this._teardownOverlayCtrl();
}
}
get _overlayInvokerNode() {
@ -171,69 +130,51 @@ export const OverlayMixin = dedupeMixin(
return contentNode || this._cachedOverlayContentNode;
}
_renderOverlayContent() {
render(this._overlayTemplate(), this.__contentParent, {
scopeName: this.localName,
eventContext: this,
_setupOverlayCtrl() {
this._overlayCtrl = this._defineOverlay({
contentNode: this._overlayContentNode,
invokerNode: this._overlayInvokerNode,
});
this.__syncToOverlayController();
this.__setupSyncFromOverlayController();
this._setupOpenCloseListeners();
}
_createOverlay() {
let contentNode;
if (this.__managesOverlayViaTemplate) {
this.__contentParent = document.createElement('div');
this._renderOverlayContent();
contentNode = this.__contentParent.firstElementChild;
} else {
contentNode = this._overlayContentNode;
}
// Why no template support for invokerNode?
// -> Because this node will always be managed by the Subclasser and should
// reside in the dom of the sub class. A reference to a rendered node suffices.
const invokerNode = this._overlayInvokerNode;
this._overlayCtrl = this._defineOverlay({ contentNode, invokerNode });
_teardownOverlayCtrl() {
this._teardownOpenCloseListeners();
this.__teardownSyncFromOverlayController();
this._overlayCtrl.teardown();
}
// FIXME: We add an overlay slot to the wrapper, but the content node already has a slot="content"
// This is a big problem, because slots should be direct children of its host element.
// Putting the shadow outlet slot in between breaks that. https://github.com/ing-bank/lion/issues/382
/**
* @desc Should be called by Subclasser for local overlay support in shadow roots
* Create an outlet slot in shadow dom that our local overlay can pass through
*/
_createOutletForLocalOverlay() {
const outlet = document.createElement('slot');
outlet.name = '_overlay-shadow-outlet';
this.shadowRoot.appendChild(outlet);
this._overlayCtrl._contentNodeWrapper.slot = '_overlay-shadow-outlet';
__setupSyncFromOverlayController() {
this.__onOverlayCtrlShow = () => {
this.opened = true;
};
this.__onOverlayCtrlHide = () => {
this.opened = false;
};
this.__onBeforeShow = () => {
this.dispatchEvent(new Event('before-show'));
};
this._overlayCtrl.addEventListener('show', this.__onOverlayCtrlShow);
this._overlayCtrl.addEventListener('hide', this.__onOverlayCtrlHide);
this._overlayCtrl.addEventListener('before-show', this.__onBeforeShow);
}
/**
* @desc Two options for a Subclasser:
* - 1: Define a template in `._overlayTemplate`. In this case the overlay content is
* predefined and thus belongs to the web component. Examples: datepicker.
* - 2: Define a getter `_overlayContentNode` that returns a node reference to a (content
* projected) node. Used when Application Developer is in charge of the content. Examples:
* popover, dialog, bottom sheet, dropdown, tooltip, select, combobox etc.
*/
get __managesOverlayViaTemplate() {
return Boolean(this._overlayTemplate);
__teardownSyncFromOverlayController() {
this._overlayCtrl.removeEventListener('show', this.__onOverlayCtrlShow);
this._overlayCtrl.removeEventListener('hide', this.__onOverlayCtrlHide);
this._overlayCtrl.removeEventListener('before-show', this.__onBeforeShow);
}
__syncOpened() {
if (this._opened) {
__syncToOverlayController() {
if (this.opened) {
this._overlayCtrl.show();
} else {
this._overlayCtrl.hide();
}
}
__syncPopper() {
if (this._overlayCtrl) {
// TODO: Use updateConfig directly.. But maybe we can remove this entirely.
this._overlayCtrl.updatePopperConfig(this.config.popperConfig);
}
}
},
);

View file

@ -1,7 +1,5 @@
import { storiesOf, html, withKnobs } from '@open-wc/demoing-storybook';
import { css, render, LitElement } from '@lion/core';
import '@lion/icon/lion-icon.js';
import '@lion/button/lion-button.js';
import {
withBottomSheetConfig,
withDropdownConfig,
@ -70,7 +68,7 @@ const overlayDemoStyle = css`
flex-direction: column;
}
.overlay {
.demo-overlay {
display: block;
position: absolute;
font-size: 16px;
@ -80,7 +78,7 @@ const overlayDemoStyle = css`
padding: 8px;
}
.overlay lion-button {
.demo-overlay button {
color: black;
}
@ -93,11 +91,6 @@ const overlayDemoStyle = css`
customElements.define(
'lion-demo-overlay',
class extends OverlayMixin(LitElement) {
constructor() {
super();
this.closeEventName = 'demo-overlay-close';
}
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
return {
@ -107,24 +100,20 @@ customElements.define(
_setupOpenCloseListeners() {
this.__toggle = () => {
console.log('toggle!');
this.opened = !this.opened;
};
console.log(this._overlayCtrl.invokerNode, this, this.__toggle);
this._overlayCtrl.invokerNode.addEventListener('click', this.__toggle);
this._overlayCtrl.invokerNode.addEventListener('click', () => console.log('ay'));
this._overlayInvokerNode.addEventListener('click', this.__toggle);
}
_teardownOpenCloseListeners() {
console.log('teardown for', this);
this._overlayCtrl.invokerNode.removeEventListener('click', this.__toggle);
this._overlayInvokerNode.removeEventListener('click', this.__toggle);
}
render() {
return html`
<slot name="invoker"></slot>
<slot name="content"></slot>
<slot name="_overlay-shadow-outlet"></slot>
`;
}
},
@ -145,25 +134,25 @@ storiesOf('Overlay System | Overlay as a WC', module)
</p>
<p>
To close your overlay from some action performed inside the content slot, fire a
<code>close</code> event.
<code>hide</code> event.
</p>
<p>
For the overlay to close, it will need to bubble to the content slot (use
<code>bubbles: true</code>. Also <code>composed: true</code> if it needs to traverse shadow
boundaries)
<code>bubbles: true</code>. If absolutely needed <code>composed: true</code> can be used to
traverse shadow boundaries)
</p>
<p>The demo below demonstrates this</p>
<div class="demo-box">
<lion-demo-overlay>
<lion-button slot="invoker">Overlay</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Overlay</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-demo-overlay>
</div>
@ -174,15 +163,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
<lion-demo-overlay
.config=${{ hasBackdrop: true, trapsKeyboardFocus: true, viewportConfig: { placement } }}
>
<lion-button slot="invoker">Overlay ${placement}</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Overlay ${placement}</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-demo-overlay>
`;
@ -204,27 +193,25 @@ storiesOf('Overlay System | Overlay as a WC', module)
${overlayDemoStyle}
</style>
<lion-demo-overlay .config=${{ ...withModalDialogConfig() }}>
<lion-button slot="invoker">Overlay</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Overlay</button>
<div slot="content" class="demo-overlay">
<div>
Hello! This is a notification.
<lion-button
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
>Close</lion-button
>
<button @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}>
Close
</button>
<lion-demo-overlay
.config=${{ ...withModalDialogConfig(), viewportConfig: { placement: 'top' } }}
>
<lion-button slot="invoker">Open child</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Open child</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-demo-overlay>
</div>
@ -242,15 +229,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
<lion-demo-overlay
.config=${{ placementMode: 'local', popperConfig: { placement: 'bottom-start' } }}
>
<lion-button slot="invoker">Overlay</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Overlay</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-demo-overlay>
</div>
@ -307,15 +294,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
.add('Switch overlays configuration', () => {
const overlay = renderOffline(html`
<lion-demo-overlay .config=${{ ...withBottomSheetConfig() }}>
<lion-button slot="invoker">Overlay</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Overlay</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-demo-overlay>
`);
@ -376,15 +363,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
}
}}
>
<lion-button slot="invoker">Overlay</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Overlay</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e =>
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>
</button>
</div>
</lion-demo-overlay>
`,
@ -411,7 +398,7 @@ storiesOf('Overlay System | Overlay as a WC', module)
}}
>UK</span
>
<div slot="content" class="overlay">
<div slot="content" class="demo-overlay">
United Kingdom
</div>
</lion-demo-overlay>
@ -475,13 +462,13 @@ storiesOf('Overlay System | Overlay as a WC', module)
: { popperConfig: { placement: text('local config', 'top-start') } }),
}}
>
<lion-button slot="invoker">Overlay</lion-button>
<div slot="content" class="overlay">
<button slot="invoker">Overlay</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<lion-button
<button
class="close-button"
@click=${e => e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
></button
>
</div>
</lion-demo-overlay>

View file

@ -233,7 +233,7 @@ storiesOf('Overlay System | Behavior Features', module)
${this.options[(this.options.indexOf(this.placement) + 1) % this.options.length]}
position
</button>
<button @click="${() => this.dispatchEvent(new CustomEvent('close'))}">Close</button>
<button @click="${() => this.dispatchEvent(new Event('hide'))}">Close</button>
`;
}
@ -265,9 +265,6 @@ storiesOf('Overlay System | Behavior Features', module)
element.addEventListener('toggle-placement', e => {
overlayCtrl.updateConfig({ viewportConfig: { placement: e.detail } });
});
element.addEventListener('close', () => {
overlayCtrl.hide();
});
return html`
<style>
${globalOverlayDemoStyle}

View file

@ -0,0 +1,57 @@
import { expect, fixture, html, aTimeout } from '@open-wc/testing';
export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) {
describe(`OverlayMixin${suffix}`, () => {
let el;
beforeEach(async () => {
el = await fixture(html`
<${tag}>
<div slot="content">content of the overlay</div>
<button slot="invoker">invoker button</button>
</${tag}>
`);
});
it('should not be opened by default', async () => {
expect(el.opened).to.be.false;
expect(el._overlayCtrl.isShown).to.be.false;
});
it('syncs opened to overlayController', async () => {
el.opened = true;
expect(el.opened).to.be.true;
await aTimeout(); // overlayCtrl show/hide is async
expect(el._overlayCtrl.isShown).to.be.true;
el.opened = false;
expect(el.opened).to.be.false;
await aTimeout(0); // overlayCtrl show/hide is async
expect(el._overlayCtrl.isShown).to.be.false;
});
it('syncs overlayController to opened', async () => {
expect(el.opened).to.be.false;
await el._overlayCtrl.show();
expect(el.opened).to.be.true;
await el._overlayCtrl.hide();
expect(el.opened).to.be.false;
});
it('should respond to initially and dynamically setting the config', async () => {
const itEl = await fixture(html`
<${tag} .config=${{ trapsKeyboardFocus: false, viewportConfig: { placement: 'top' } }}>
<div slot="content">content of the overlay</div>
<button slot="invoker">invoker button</button>
</${tag}>
`);
itEl.opened = true;
await itEl.updateComplete;
expect(itEl._overlayCtrl.trapsKeyboardFocus).to.be.false;
itEl.config = { viewportConfig: { placement: 'left' } };
expect(itEl._overlayCtrl.viewportConfig.placement).to.equal('left');
});
});
}

View file

@ -314,6 +314,33 @@ describe('OverlayController', () => {
});
});
describe('hidesOnHideEventInContentNode', () => {
it('hides content on hide event within the content ', async () => {
const ctrl = new OverlayController({
...withGlobalTestConfig(),
hidesOnHideEventInContentNode: true,
contentNode: fixtureSync(html`
<div>
my content
<button @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}>
x
</button>
</div>
`),
});
await ctrl.show();
const closeBtn = ctrl.contentNode.querySelector('button');
closeBtn.click();
expect(ctrl.isShown).to.be.false;
});
it('does stop propagation of the "hide" event to not pollute the event stack and to prevent side effects', () => {
// TODO: how to test this?
});
});
describe('hidesOnOutsideClick', () => {
it('hides on outside click', async () => {
const contentNode = await fixture('<div>Content</div>');

View file

@ -0,0 +1,12 @@
import { defineCE, unsafeStatic } from '@open-wc/testing';
import { LitElement } from '@lion/core';
import { runOverlayMixinSuite } from '../test-suites/OverlayMixin.suite.js';
import { OverlayMixin } from '../src/OverlayMixin.js';
const tagString = defineCE(class extends OverlayMixin(LitElement) {});
const tag = unsafeStatic(tagString);
runOverlayMixinSuite({
tagString,
tag,
});

View file

@ -182,12 +182,6 @@ export class LionSelectRich extends OverlayMixin(
if (super.connectedCallback) {
super.connectedCallback();
}
this.__setupOverlay();
this.__setupInvokerNode();
this.__setupListboxNode();
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
}
disconnectedCallback() {
@ -202,6 +196,12 @@ export class LionSelectRich extends OverlayMixin(
firstUpdated(c) {
super.firstUpdated(c);
this.__setupOverlay();
this.__setupInvokerNode();
this.__setupListboxNode();
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
this.__toggleInvokerDisabled();
}
@ -243,6 +243,14 @@ export class LionSelectRich extends OverlayMixin(
return this.querySelector('[slot="input"]');
}
render() {
return html`
${this.labelTemplate()} ${this.helpTextTemplate()} ${this.inputGroupTemplate()}
${this.feedbackTemplate()}
<slot name="_overlay-shadow-outlet"></slot>
`;
}
updated(changedProps) {
super.updated(changedProps);

View file

@ -247,7 +247,7 @@ describe('lion-select-rich', () => {
</lion-select-rich>
`);
el._invokerNode.click();
await el.updateComplete;
await aTimeout();
expect(el.opened).to.be.true;
});
@ -258,7 +258,7 @@ describe('lion-select-rich', () => {
</lion-select-rich>
`);
el._invokerNode.click();
await el.updateComplete;
await aTimeout();
expect(el.opened).to.be.true;
});

View file

@ -36,8 +36,6 @@
"@lion/overlays": "^0.6.4"
},
"devDependencies": {
"@lion/button": "^0.3.43",
"@lion/icon": "^0.2.9",
"@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4"
}

View file

@ -74,6 +74,7 @@ export class LionTooltip extends OverlayMixin(LitElement) {
return html`
<slot name="invoker"></slot>
<slot name="content"></slot>
<slot name="_overlay-shadow-outlet"></slot>
`;
}
}

View file

@ -1,8 +1,6 @@
import { storiesOf, html, withKnobs, object, text } from '@open-wc/demoing-storybook';
import { css } from '@lion/core';
import '@lion/icon/lion-icon.js';
import '@lion/button/lion-button.js';
import '../lion-tooltip.js';
const tooltipDemoStyle = css`
@ -53,7 +51,7 @@ storiesOf('Overlays Specific WC|Tooltip', module)
</style>
<div class="demo-box">
<lion-tooltip .config=${{ popperConfig: { placement: 'right' } }}>
<lion-button slot="invoker">Tooltip</lion-button>
<button slot="invoker">Tooltip</button>
<div slot="content" class="demo-tooltip">Hello there!</div>
</lion-tooltip>
</div>
@ -67,19 +65,19 @@ storiesOf('Overlays Specific WC|Tooltip', module)
</style>
<div class="demo-box_placements">
<lion-tooltip .config=${{ popperConfig: { placement: 'top' } }}>
<lion-button slot="invoker">Top</lion-button>
<button slot="invoker">Top</button>
<div slot="content" class="demo-tooltip">Its top placement</div>
</lion-tooltip>
<lion-tooltip .config=${{ popperConfig: { placement: 'right' } }}>
<lion-button slot="invoker">Right</lion-button>
<button slot="invoker">Right</button>
<div slot="content" class="demo-tooltip">Its right placement</div>
</lion-tooltip>
<lion-tooltip .config=${{ popperConfig: { placement: 'bottom' } }}>
<lion-button slot="invoker">Bottom</lion-button>
<button slot="invoker">Bottom</button>
<div slot="content" class="demo-tooltip">Its bottom placement</div>
</lion-tooltip>
<lion-tooltip .config=${{ popperConfig: { placement: 'left' } }}>
<lion-button slot="invoker">Left</lion-button>
<button slot="invoker">Left</button>
<div slot="content" class="demo-tooltip">Its left placement</div>
</lion-tooltip>
</div>
@ -119,7 +117,7 @@ storiesOf('Overlays Specific WC|Tooltip', module)
}),
}}"
>
<lion-button slot="invoker">${text('Invoker text', 'Hover me!')}</lion-button>
<button slot="invoker">${text('Invoker text', 'Hover me!')}</button>
<div slot="content" class="demo-tooltip">${text('Content text', 'Hello, World!')}</div>
</lion-tooltip>
</div>

View file

@ -1,24 +1,26 @@
import { expect, fixture, html } from '@open-wc/testing';
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
import { runOverlayMixinSuite } from '@lion/overlays/test-suites/OverlayMixin.suite.js';
import '../lion-tooltip.js';
describe('lion-tooltip', () => {
describe('Basic', () => {
it('should not be shown by default', async () => {
const el = await fixture(html`
<lion-tooltip>
<div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button>
</lion-tooltip>
`);
expect(el._overlayCtrl.isShown).to.equal(false);
});
describe('Integration tests', () => {
const tagString = 'lion-tooltip';
const tag = unsafeStatic(tagString);
runOverlayMixinSuite({
tagString,
tag,
suffix: ' for lion-tooltip',
});
});
describe('Basic', () => {
it('should show content on mouseenter and hide on mouseleave', async () => {
const el = await fixture(html`
<lion-tooltip>
<div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button>
<button slot="invoker">Tooltip button</button>
</lion-tooltip>
`);
const eventMouseEnter = new Event('mouseenter');
@ -35,7 +37,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html`
<lion-tooltip>
<div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button>
<button slot="invoker">Tooltip button</button>
</lion-tooltip>
`);
const eventMouseEnter = new Event('mouseenter');
@ -52,7 +54,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html`
<lion-tooltip>
<div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button>
<button slot="invoker">Tooltip button</button>
</lion-tooltip>
`);
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
@ -70,7 +72,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html`
<lion-tooltip>
<div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button>
<button slot="invoker">Tooltip button</button>
</lion-tooltip>
`);
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
@ -90,7 +92,7 @@ describe('lion-tooltip', () => {
<div slot="content">
This is Tooltip using <strong id="click_overlay">overlay</strong>
</div>
<lion-button slot="invoker">Tooltip button</lion-button>
<button slot="invoker">Tooltip button</button>
</lion-tooltip>
`);
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
@ -106,7 +108,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html`
<lion-tooltip>
<div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button>
<button slot="invoker">Tooltip button</button>
</lion-tooltip>
`);

View file

@ -2088,7 +2088,7 @@
wallaby-webpack "^3.0.0"
webpack "^4.28.0"
"@open-wc/testing@^2.3.4", "@open-wc/testing@^2.3.9":
"@open-wc/testing@^2.3.4":
version "2.3.9"
resolved "https://registry.yarnpkg.com/@open-wc/testing/-/testing-2.3.9.tgz#048bb3122d989cf0df96611513aaec7738964e3d"
integrity sha512-5pKtHNP/73y9VWAwXOdxf4uzKVAtCowSdy4B6It4iETq8RshkAtKJbJBj+iQSU81pG6jOgSNPlGYeU01/CXaxw==