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" "@lion/overlays": "^0.6.4"
}, },
"devDependencies": { "devDependencies": {
"@lion/button": "^0.3.43",
"@lion/icon": "^0.2.9",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4" "@open-wc/testing": "^2.3.4"
} }

View file

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

View file

@ -1,7 +1,5 @@
import { storiesOf, html, withKnobs, object } from '@open-wc/demoing-storybook'; import { storiesOf, html, withKnobs, object } from '@open-wc/demoing-storybook';
import { css } from '@lion/core'; import { css } from '@lion/core';
import '@lion/icon/lion-icon.js';
import '@lion/button/lion-button.js';
import '../lion-dialog.js'; import '../lion-dialog.js';
const dialogDemoStyle = css` const dialogDemoStyle = css`
@ -67,24 +65,25 @@ storiesOf('Overlays Specific WC | Dialog', module)
</p> </p>
<p> <p>
To close your dialog from some action performed inside the content slot, fire a To close your dialog from some action performed inside the content slot, fire a
<code>close</code> event. <code>hide</code> event.
</p> </p>
<p> <p>
For the dialog to close, it will need to bubble to the content slot (use 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 <code>bubbles: true</code>. If absolutely needed <code>composed: true</code> can be used to
boundaries) traverse shadow boundaries)
</p> </p>
<p>The demo below demonstrates this</p> <p>The demo below demonstrates this</p>
<div class="demo-box"> <div class="demo-box">
<lion-dialog> <lion-dialog>
<lion-button slot="invoker">Dialog</lion-button> <button slot="invoker">Dialog</button>
<div slot="content" class="dialog"> <div slot="content" class="dialog">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-dialog> </lion-dialog>
</div> </div>
@ -93,14 +92,15 @@ storiesOf('Overlays Specific WC | Dialog', module)
.add('Custom configuration', () => { .add('Custom configuration', () => {
const dialog = placement => html` const dialog = placement => html`
<lion-dialog .config=${{ viewportConfig: { placement } }}> <lion-dialog .config=${{ viewportConfig: { placement } }}>
<lion-button slot="invoker">Dialog ${placement}</lion-button> <button slot="invoker">Dialog ${placement}</button>
<div slot="content" class="dialog"> <div slot="content" class="dialog">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-dialog> </lion-dialog>
`; `;
@ -118,14 +118,15 @@ storiesOf('Overlays Specific WC | Dialog', module)
.add('Toggle placement with knobs', () => { .add('Toggle placement with knobs', () => {
const dialog = html` const dialog = html`
<lion-dialog .config=${object('config', { viewportConfig: { placement: 'center' } })}> <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"> <div slot="content" class="dialog">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => e.target.dispatchEvent(new Event('dialog-close', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-dialog> </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'; import '../lion-dialog.js';
// Smoke tests dialog
describe('lion-dialog', () => { describe('lion-dialog', () => {
describe('Basic', () => { describe('Integration tests', () => {
it('should not be shown by default', async () => { const tagString = 'lion-dialog';
const el = await fixture(html` const tag = unsafeStatic(tagString);
<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;
});
runOverlayMixinSuite({
tagString,
tag,
suffix: ' for lion-dialog',
});
});
describe('Basic', () => {
it('should show content on invoker click', async () => { it('should show content on invoker click', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-dialog> <lion-dialog>
<div slot="content" class="dialog"> <div slot="content" class="dialog">
Hey there Hey there
</div> </div>
<lion-button slot="invoker">Popup button</lion-button> <button slot="invoker">Popup button</button>
</lion-dialog> </lion-dialog>
`); `);
const invoker = el.querySelector('[slot="invoker"]'); const invoker = el.querySelector('[slot="invoker"]');
invoker.click(); invoker.click();
expect(el._overlayCtrl.isShown).to.be.true; expect(el.opened).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',
),
);
}); });
}); });
}); });

View file

@ -90,14 +90,9 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
]; ];
} }
constructor() { __dispatchHideEvent() {
super();
this.__dispatchCloseEvent = this.__dispatchCloseEvent.bind(this);
}
__dispatchCloseEvent() {
// Designed to work in conjunction with ModalDialogController // 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() { render() {
@ -109,7 +104,7 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
<slot name="heading"></slot> <slot name="heading"></slot>
</h1> </h1>
<button <button
@click="${this.__dispatchCloseEvent}" @click="${this.__dispatchHideEvent}"
id="close-button" id="close-button"
title="${this.msgLit('lion-calendar-overlay-frame:close')}" title="${this.msgLit('lion-calendar-overlay-frame:close')}"
aria-label="${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. * this is our source to give as .contentNode to OverlayController.
* Important: do not change the name of this method. * Important: do not change the name of this method.
*/ */
// TODO: Refactor to new overlay system public API --> @close=${() => { this.opened = false; }}
_overlayTemplate() { _overlayTemplate() {
// TODO: add performance optimization to only render the calendar if needed
return html` return html`
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}> <lion-calendar-overlay-frame>
<span slot="heading">${this.calendarHeading}</span> <span slot="heading">${this.calendarHeading}</span>
${this._calendarTemplate()} ${this._calendarTemplate()}
</lion-calendar-overlay-frame> </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 * Subclassers can replace this with their custom extension of
* LionCalendar, like `<my-calendar id="calendar"></my-calendar>` * LionCalendar, like `<my-calendar id="calendar"></my-calendar>`
@ -331,4 +338,11 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
get _overlayInvokerNode() { get _overlayInvokerNode() {
return this._invokerElement; 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' [//]: # '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. Supports different types of overlays like dialogs, toasts, tooltips, dropdown, etc.
Manages their position on the screen relative to other elements, including other overlays. 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: - lion-overlay web component:
- Show content when clicking the invoker - 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 - 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 - [**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'; import '@lion/overlays/lion-overlay.js';
html` html`
<lion-overlay.config=${{ <lion-overlay .config=${{
placementMode: 'global', placementMode: 'global',
viewportConfig: { placement: 'bottom-right' }, viewportConfig: { placement: 'bottom-right' },
}}> }}>
<div slot="content"> <div slot="content">
This is an overlay This is an overlay
<button <button
@click=${e => e.target.dispatchEvent(new Event('overlay-close', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
>x</button> >x</button>
<div> <div>
<button slot="invoker"> <button slot="invoker">

View file

@ -32,6 +32,8 @@ All boolean flags default to 'false'.
```text ```text
- {Boolean} trapsKeyboardFocus - rotates tab. - {Boolean} trapsKeyboardFocus - rotates tab.
- {Boolean} hidesOnEsc - hides the overlay when pressing [esc]. - {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. - {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} hasBackdrop - whether it should have a backdrop. (local mode only)
- {Boolean} isBlocking - hides other overlays when multiple are opened. - {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", "stories",
"test", "test",
"test-helpers", "test-helpers",
"test-suites",
"translations", "translations",
"*.js" "*.js"
], ],
@ -37,8 +38,6 @@
"popper.js": "^1.15.0" "popper.js": "^1.15.0"
}, },
"devDependencies": { "devDependencies": {
"@lion/button": "^0.3.43",
"@lion/icon": "^0.2.9",
"@open-wc/demoing-storybook": "^0.2.0", "@open-wc/demoing-storybook": "^0.2.0",
"@open-wc/testing": "^2.3.4", "@open-wc/testing": "^2.3.4",
"@open-wc/testing-helpers": "^1.0.0", "@open-wc/testing-helpers": "^1.0.0",

View file

@ -33,6 +33,7 @@ export class OverlayController {
trapsKeyboardFocus: false, trapsKeyboardFocus: false,
hidesOnEsc: false, hidesOnEsc: false,
hidesOnOutsideClick: false, hidesOnOutsideClick: false,
hidesOnHideEventInContentNode: true,
isTooltip: false, isTooltip: false,
handlesUserInteraction: false, handlesUserInteraction: false,
handlesAccessibility: false, handlesAccessibility: false,
@ -336,6 +337,9 @@ export class OverlayController {
if (this.hidesOnOutsideClick) { if (this.hidesOnOutsideClick) {
this._handleHidesOnOutsideClick({ phase }); this._handleHidesOnOutsideClick({ phase });
} }
if (this.hidesOnHideEventInContentNode) {
this._handleHidesOnHideEventInContentNode({ phase });
}
if (this.handlesAccessibility) { if (this.handlesAccessibility) {
this._handleAccessibility({ phase }); this._handleAccessibility({ phase });
} }
@ -483,10 +487,26 @@ export class OverlayController {
if (phase === 'show') { if (phase === 'show') {
this.__escKeyHandler = ev => ev.key === 'Escape' && this.hide(); this.__escKeyHandler = ev => ev.key === 'Escape' && this.hide();
this.contentNode.addEventListener('keyup', this.__escKeyHandler); 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') { } else if (phase === 'hide') {
this.contentNode.removeEventListener('keyup', this.__escKeyHandler); 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'; import { OverlayController } from './OverlayController.js';
/** /**
@ -16,54 +16,26 @@ export const OverlayMixin = dedupeMixin(
type: Boolean, type: Boolean,
reflect: true, reflect: true,
}, },
config: {
type: Object,
},
closeEventName: {
type: String,
},
}; };
} }
constructor() { constructor() {
super(); super();
this.opened = false;
this.config = {}; 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() { get config() {
return this._config; return this.__config;
} }
set config(value) { set config(value) {
if (this._overlayCtrl) { if (this._overlayCtrl) {
this._overlayCtrl.updateConfig(value); 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` * @overridable method `_defineOverlay`
* @desc returns an instance of a (dynamic) overlay controller * @desc returns an instance of a (dynamic) overlay controller
@ -88,7 +60,19 @@ export const OverlayMixin = dedupeMixin(
*/ */
// eslint-disable-next-line // eslint-disable-next-line
_defineOverlayConfig() { _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 // eslint-disable-next-line class-methods-use-this
_teardownOpenCloseListeners() {} _teardownOpenCloseListeners() {}
connectedCallback() { firstUpdated(changedProperties) {
if (super.connectedCallback) { super.firstUpdated(changedProperties);
super.connectedCallback(); // we setup in firstUpdated so we can use nodes from light and shadowDom
} this._setupOverlayCtrl();
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();
}
} }
disconnectedCallback() { disconnectedCallback() {
if (super.disconnectedCallback) { if (super.disconnectedCallback) {
super.disconnectedCallback(); super.disconnectedCallback();
} }
this.opened = false; if (this._overlayCtrl) {
this._overlayCtrl.contentNode.removeEventListener(this.closeEventName, this.__close); this._teardownOverlayCtrl();
this._teardownOpenCloseListeners(); }
this._overlayCtrl.teardown();
} }
get _overlayInvokerNode() { get _overlayInvokerNode() {
@ -171,69 +130,51 @@ export const OverlayMixin = dedupeMixin(
return contentNode || this._cachedOverlayContentNode; return contentNode || this._cachedOverlayContentNode;
} }
_renderOverlayContent() { _setupOverlayCtrl() {
render(this._overlayTemplate(), this.__contentParent, { this._overlayCtrl = this._defineOverlay({
scopeName: this.localName, contentNode: this._overlayContentNode,
eventContext: this, invokerNode: this._overlayInvokerNode,
}); });
this.__syncToOverlayController();
this.__setupSyncFromOverlayController();
this._setupOpenCloseListeners();
} }
_createOverlay() { _teardownOverlayCtrl() {
let contentNode; this._teardownOpenCloseListeners();
if (this.__managesOverlayViaTemplate) { this.__teardownSyncFromOverlayController();
this.__contentParent = document.createElement('div'); this._overlayCtrl.teardown();
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 });
} }
// FIXME: We add an overlay slot to the wrapper, but the content node already has a slot="content" __setupSyncFromOverlayController() {
// This is a big problem, because slots should be direct children of its host element. this.__onOverlayCtrlShow = () => {
// Putting the shadow outlet slot in between breaks that. https://github.com/ing-bank/lion/issues/382 this.opened = true;
/** };
* @desc Should be called by Subclasser for local overlay support in shadow roots this.__onOverlayCtrlHide = () => {
* Create an outlet slot in shadow dom that our local overlay can pass through this.opened = false;
*/ };
_createOutletForLocalOverlay() { this.__onBeforeShow = () => {
const outlet = document.createElement('slot'); this.dispatchEvent(new Event('before-show'));
outlet.name = '_overlay-shadow-outlet'; };
this.shadowRoot.appendChild(outlet);
this._overlayCtrl._contentNodeWrapper.slot = '_overlay-shadow-outlet'; this._overlayCtrl.addEventListener('show', this.__onOverlayCtrlShow);
this._overlayCtrl.addEventListener('hide', this.__onOverlayCtrlHide);
this._overlayCtrl.addEventListener('before-show', this.__onBeforeShow);
} }
/** __teardownSyncFromOverlayController() {
* @desc Two options for a Subclasser: this._overlayCtrl.removeEventListener('show', this.__onOverlayCtrlShow);
* - 1: Define a template in `._overlayTemplate`. In this case the overlay content is this._overlayCtrl.removeEventListener('hide', this.__onOverlayCtrlHide);
* predefined and thus belongs to the web component. Examples: datepicker. this._overlayCtrl.removeEventListener('before-show', this.__onBeforeShow);
* - 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);
} }
__syncOpened() { __syncToOverlayController() {
if (this._opened) { if (this.opened) {
this._overlayCtrl.show(); this._overlayCtrl.show();
} else { } else {
this._overlayCtrl.hide(); 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 { storiesOf, html, withKnobs } from '@open-wc/demoing-storybook';
import { css, render, LitElement } from '@lion/core'; import { css, render, LitElement } from '@lion/core';
import '@lion/icon/lion-icon.js';
import '@lion/button/lion-button.js';
import { import {
withBottomSheetConfig, withBottomSheetConfig,
withDropdownConfig, withDropdownConfig,
@ -70,7 +68,7 @@ const overlayDemoStyle = css`
flex-direction: column; flex-direction: column;
} }
.overlay { .demo-overlay {
display: block; display: block;
position: absolute; position: absolute;
font-size: 16px; font-size: 16px;
@ -80,7 +78,7 @@ const overlayDemoStyle = css`
padding: 8px; padding: 8px;
} }
.overlay lion-button { .demo-overlay button {
color: black; color: black;
} }
@ -93,11 +91,6 @@ const overlayDemoStyle = css`
customElements.define( customElements.define(
'lion-demo-overlay', 'lion-demo-overlay',
class extends OverlayMixin(LitElement) { class extends OverlayMixin(LitElement) {
constructor() {
super();
this.closeEventName = 'demo-overlay-close';
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() { _defineOverlayConfig() {
return { return {
@ -107,24 +100,20 @@ customElements.define(
_setupOpenCloseListeners() { _setupOpenCloseListeners() {
this.__toggle = () => { this.__toggle = () => {
console.log('toggle!');
this.opened = !this.opened; this.opened = !this.opened;
}; };
this._overlayInvokerNode.addEventListener('click', this.__toggle);
console.log(this._overlayCtrl.invokerNode, this, this.__toggle);
this._overlayCtrl.invokerNode.addEventListener('click', this.__toggle);
this._overlayCtrl.invokerNode.addEventListener('click', () => console.log('ay'));
} }
_teardownOpenCloseListeners() { _teardownOpenCloseListeners() {
console.log('teardown for', this); this._overlayInvokerNode.removeEventListener('click', this.__toggle);
this._overlayCtrl.invokerNode.removeEventListener('click', this.__toggle);
} }
render() { render() {
return html` return html`
<slot name="invoker"></slot> <slot name="invoker"></slot>
<slot name="content"></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>
<p> <p>
To close your overlay from some action performed inside the content slot, fire a To close your overlay from some action performed inside the content slot, fire a
<code>close</code> event. <code>hide</code> event.
</p> </p>
<p> <p>
For the overlay to close, it will need to bubble to the content slot (use 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 <code>bubbles: true</code>. If absolutely needed <code>composed: true</code> can be used to
boundaries) traverse shadow boundaries)
</p> </p>
<p>The demo below demonstrates this</p> <p>The demo below demonstrates this</p>
<div class="demo-box"> <div class="demo-box">
<lion-demo-overlay> <lion-demo-overlay>
<lion-button slot="invoker">Overlay</lion-button> <button slot="invoker">Overlay</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-demo-overlay> </lion-demo-overlay>
</div> </div>
@ -174,15 +163,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
<lion-demo-overlay <lion-demo-overlay
.config=${{ hasBackdrop: true, trapsKeyboardFocus: true, viewportConfig: { placement } }} .config=${{ hasBackdrop: true, trapsKeyboardFocus: true, viewportConfig: { placement } }}
> >
<lion-button slot="invoker">Overlay ${placement}</lion-button> <button slot="invoker">Overlay ${placement}</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-demo-overlay> </lion-demo-overlay>
`; `;
@ -204,27 +193,25 @@ storiesOf('Overlay System | Overlay as a WC', module)
${overlayDemoStyle} ${overlayDemoStyle}
</style> </style>
<lion-demo-overlay .config=${{ ...withModalDialogConfig() }}> <lion-demo-overlay .config=${{ ...withModalDialogConfig() }}>
<lion-button slot="invoker">Overlay</lion-button> <button slot="invoker">Overlay</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
<div> <div>
Hello! This is a notification. Hello! This is a notification.
<lion-button <button @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}>
@click=${e => Close
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))} </button>
>Close</lion-button
>
<lion-demo-overlay <lion-demo-overlay
.config=${{ ...withModalDialogConfig(), viewportConfig: { placement: 'top' } }} .config=${{ ...withModalDialogConfig(), viewportConfig: { placement: 'top' } }}
> >
<lion-button slot="invoker">Open child</lion-button> <button slot="invoker">Open child</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-demo-overlay> </lion-demo-overlay>
</div> </div>
@ -242,15 +229,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
<lion-demo-overlay <lion-demo-overlay
.config=${{ placementMode: 'local', popperConfig: { placement: 'bottom-start' } }} .config=${{ placementMode: 'local', popperConfig: { placement: 'bottom-start' } }}
> >
<lion-button slot="invoker">Overlay</lion-button> <button slot="invoker">Overlay</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-demo-overlay> </lion-demo-overlay>
</div> </div>
@ -307,15 +294,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
.add('Switch overlays configuration', () => { .add('Switch overlays configuration', () => {
const overlay = renderOffline(html` const overlay = renderOffline(html`
<lion-demo-overlay .config=${{ ...withBottomSheetConfig() }}> <lion-demo-overlay .config=${{ ...withBottomSheetConfig() }}>
<lion-button slot="invoker">Overlay</lion-button> <button slot="invoker">Overlay</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-demo-overlay> </lion-demo-overlay>
`); `);
@ -376,15 +363,15 @@ storiesOf('Overlay System | Overlay as a WC', module)
} }
}} }}
> >
<lion-button slot="invoker">Overlay</lion-button> <button slot="invoker">Overlay</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))}
></lion-button
> >
</button>
</div> </div>
</lion-demo-overlay> </lion-demo-overlay>
`, `,
@ -411,7 +398,7 @@ storiesOf('Overlay System | Overlay as a WC', module)
}} }}
>UK</span >UK</span
> >
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
United Kingdom United Kingdom
</div> </div>
</lion-demo-overlay> </lion-demo-overlay>
@ -475,13 +462,13 @@ storiesOf('Overlay System | Overlay as a WC', module)
: { popperConfig: { placement: text('local config', 'top-start') } }), : { popperConfig: { placement: text('local config', 'top-start') } }),
}} }}
> >
<lion-button slot="invoker">Overlay</lion-button> <button slot="invoker">Overlay</button>
<div slot="content" class="overlay"> <div slot="content" class="demo-overlay">
Hello! You can close this notification here: Hello! You can close this notification here:
<lion-button <button
class="close-button" class="close-button"
@click=${e => e.target.dispatchEvent(new Event('demo-overlay-close', { bubbles: true }))} @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
></lion-button ></button
> >
</div> </div>
</lion-demo-overlay> </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]} ${this.options[(this.options.indexOf(this.placement) + 1) % this.options.length]}
position position
</button> </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 => { element.addEventListener('toggle-placement', e => {
overlayCtrl.updateConfig({ viewportConfig: { placement: e.detail } }); overlayCtrl.updateConfig({ viewportConfig: { placement: e.detail } });
}); });
element.addEventListener('close', () => {
overlayCtrl.hide();
});
return html` return html`
<style> <style>
${globalOverlayDemoStyle} ${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', () => { describe('hidesOnOutsideClick', () => {
it('hides on outside click', async () => { it('hides on outside click', async () => {
const contentNode = await fixture('<div>Content</div>'); 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) { if (super.connectedCallback) {
super.connectedCallback(); super.connectedCallback();
} }
this.__setupOverlay();
this.__setupInvokerNode();
this.__setupListboxNode();
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
} }
disconnectedCallback() { disconnectedCallback() {
@ -202,6 +196,12 @@ export class LionSelectRich extends OverlayMixin(
firstUpdated(c) { firstUpdated(c) {
super.firstUpdated(c); super.firstUpdated(c);
this.__setupOverlay();
this.__setupInvokerNode();
this.__setupListboxNode();
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
this.__toggleInvokerDisabled(); this.__toggleInvokerDisabled();
} }
@ -243,6 +243,14 @@ export class LionSelectRich extends OverlayMixin(
return this.querySelector('[slot="input"]'); return this.querySelector('[slot="input"]');
} }
render() {
return html`
${this.labelTemplate()} ${this.helpTextTemplate()} ${this.inputGroupTemplate()}
${this.feedbackTemplate()}
<slot name="_overlay-shadow-outlet"></slot>
`;
}
updated(changedProps) { updated(changedProps) {
super.updated(changedProps); super.updated(changedProps);

View file

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

View file

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

View file

@ -74,6 +74,7 @@ export class LionTooltip extends OverlayMixin(LitElement) {
return html` return html`
<slot name="invoker"></slot> <slot name="invoker"></slot>
<slot name="content"></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 { storiesOf, html, withKnobs, object, text } from '@open-wc/demoing-storybook';
import { css } from '@lion/core'; import { css } from '@lion/core';
import '@lion/icon/lion-icon.js';
import '@lion/button/lion-button.js';
import '../lion-tooltip.js'; import '../lion-tooltip.js';
const tooltipDemoStyle = css` const tooltipDemoStyle = css`
@ -53,7 +51,7 @@ storiesOf('Overlays Specific WC|Tooltip', module)
</style> </style>
<div class="demo-box"> <div class="demo-box">
<lion-tooltip .config=${{ popperConfig: { placement: 'right' } }}> <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> <div slot="content" class="demo-tooltip">Hello there!</div>
</lion-tooltip> </lion-tooltip>
</div> </div>
@ -67,19 +65,19 @@ storiesOf('Overlays Specific WC|Tooltip', module)
</style> </style>
<div class="demo-box_placements"> <div class="demo-box_placements">
<lion-tooltip .config=${{ popperConfig: { placement: 'top' } }}> <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> <div slot="content" class="demo-tooltip">Its top placement</div>
</lion-tooltip> </lion-tooltip>
<lion-tooltip .config=${{ popperConfig: { placement: 'right' } }}> <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> <div slot="content" class="demo-tooltip">Its right placement</div>
</lion-tooltip> </lion-tooltip>
<lion-tooltip .config=${{ popperConfig: { placement: 'bottom' } }}> <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> <div slot="content" class="demo-tooltip">Its bottom placement</div>
</lion-tooltip> </lion-tooltip>
<lion-tooltip .config=${{ popperConfig: { placement: 'left' } }}> <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> <div slot="content" class="demo-tooltip">Its left placement</div>
</lion-tooltip> </lion-tooltip>
</div> </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> <div slot="content" class="demo-tooltip">${text('Content text', 'Hello, World!')}</div>
</lion-tooltip> </lion-tooltip>
</div> </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'; import '../lion-tooltip.js';
describe('lion-tooltip', () => { describe('lion-tooltip', () => {
describe('Basic', () => { describe('Integration tests', () => {
it('should not be shown by default', async () => { const tagString = 'lion-tooltip';
const el = await fixture(html` const tag = unsafeStatic(tagString);
<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);
});
runOverlayMixinSuite({
tagString,
tag,
suffix: ' for lion-tooltip',
});
});
describe('Basic', () => {
it('should show content on mouseenter and hide on mouseleave', async () => { it('should show content on mouseenter and hide on mouseleave', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-tooltip> <lion-tooltip>
<div slot="content">Hey there</div> <div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button> <button slot="invoker">Tooltip button</button>
</lion-tooltip> </lion-tooltip>
`); `);
const eventMouseEnter = new Event('mouseenter'); const eventMouseEnter = new Event('mouseenter');
@ -35,7 +37,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-tooltip> <lion-tooltip>
<div slot="content">Hey there</div> <div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button> <button slot="invoker">Tooltip button</button>
</lion-tooltip> </lion-tooltip>
`); `);
const eventMouseEnter = new Event('mouseenter'); const eventMouseEnter = new Event('mouseenter');
@ -52,7 +54,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-tooltip> <lion-tooltip>
<div slot="content">Hey there</div> <div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button> <button slot="invoker">Tooltip button</button>
</lion-tooltip> </lion-tooltip>
`); `);
const invoker = Array.from(el.children).find(child => child.slot === 'invoker'); const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
@ -70,7 +72,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-tooltip> <lion-tooltip>
<div slot="content">Hey there</div> <div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button> <button slot="invoker">Tooltip button</button>
</lion-tooltip> </lion-tooltip>
`); `);
const invoker = Array.from(el.children).find(child => child.slot === 'invoker'); const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
@ -90,7 +92,7 @@ describe('lion-tooltip', () => {
<div slot="content"> <div slot="content">
This is Tooltip using <strong id="click_overlay">overlay</strong> This is Tooltip using <strong id="click_overlay">overlay</strong>
</div> </div>
<lion-button slot="invoker">Tooltip button</lion-button> <button slot="invoker">Tooltip button</button>
</lion-tooltip> </lion-tooltip>
`); `);
const invoker = Array.from(el.children).find(child => child.slot === 'invoker'); const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
@ -106,7 +108,7 @@ describe('lion-tooltip', () => {
const el = await fixture(html` const el = await fixture(html`
<lion-tooltip> <lion-tooltip>
<div slot="content">Hey there</div> <div slot="content">Hey there</div>
<lion-button slot="invoker">Tooltip button</lion-button> <button slot="invoker">Tooltip button</button>
</lion-tooltip> </lion-tooltip>
`); `);

View file

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