feat: refactor the overlay system implementations, docs and demos
This commit is contained in:
parent
29cf5ac188
commit
a5a9f975a6
40 changed files with 2482 additions and 2011 deletions
45
packages/dialog/README.md
Normal file
45
packages/dialog/README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Dialog
|
||||||
|
|
||||||
|
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||||
|
|
||||||
|
`lion-dialog` is a component wrapping a modal dialog controller
|
||||||
|
Its purpose is to make it easy to use our Overlay System declaratively
|
||||||
|
With regards to modal dialogs, this is one of the more commonly used examples of overlays.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Show content when clicking the invoker
|
||||||
|
- Respond to close event in the slot="content" element, to close the content
|
||||||
|
- Have a `.config` object to set or update the OverlayController's configuration
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i --save @lion/dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import '@lion/dialog/lion-dialog.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
html`
|
||||||
|
<lion-dialog>
|
||||||
|
<div slot="content" class="tooltip" .config=${{
|
||||||
|
viewportConfig: { placement: 'bottom-right' },
|
||||||
|
}}>
|
||||||
|
This is a dialog
|
||||||
|
<button
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>x</button>
|
||||||
|
<div>
|
||||||
|
<button slot="invoker">
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</lion-dialog>
|
||||||
|
`;
|
||||||
|
```
|
||||||
1
packages/dialog/index.js
Normal file
1
packages/dialog/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { LionDialog } from './src/LionDialog.js';
|
||||||
3
packages/dialog/lion-dialog.js
Normal file
3
packages/dialog/lion-dialog.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionDialog } from './src/LionDialog.js';
|
||||||
|
|
||||||
|
customElements.define('lion-dialog', LionDialog);
|
||||||
42
packages/dialog/package.json
Normal file
42
packages/dialog/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"name": "@lion/dialog",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Show relative overlay content on click, as a webcomponent",
|
||||||
|
"author": "ing-bank",
|
||||||
|
"homepage": "https://github.com/ing-bank/lion/",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ing-bank/lion.git",
|
||||||
|
"directory": "packages/dialog"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "../../scripts/npm-prepublish.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"lion",
|
||||||
|
"web-components",
|
||||||
|
"dialog"
|
||||||
|
],
|
||||||
|
"main": "index.js",
|
||||||
|
"module": "index.js",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"stories",
|
||||||
|
"test",
|
||||||
|
"*.js"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@lion/core": "^0.3.0",
|
||||||
|
"@lion/overlays": "^0.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@lion/button": "^0.3.32",
|
||||||
|
"@lion/icon": "^0.2.8",
|
||||||
|
"@open-wc/demoing-storybook": "^0.2.0",
|
||||||
|
"@open-wc/testing": "^2.3.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/dialog/src/LionDialog.js
Normal file
14
packages/dialog/src/LionDialog.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { LionOverlay, OverlayController, withModalDialogConfig } from '@lion/overlays';
|
||||||
|
|
||||||
|
export class LionDialog extends LionOverlay {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_defineOverlay({ contentNode, invokerNode }) {
|
||||||
|
return new OverlayController({
|
||||||
|
...withModalDialogConfig(),
|
||||||
|
elementToFocusAfterHide: invokerNode,
|
||||||
|
contentNode,
|
||||||
|
invokerNode,
|
||||||
|
...this.config, // lit-property set by user for overrides
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
141
packages/dialog/stories/index.stories.js
Normal file
141
packages/dialog/stories/index.stories.js
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
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`
|
||||||
|
.demo-box {
|
||||||
|
width: 200px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid grey;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-box_placements {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 173px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
lion-dialog {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
color: black;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-box__column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.dialog {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
storiesOf('Overlays Specific WC | Dialog', module)
|
||||||
|
.addDecorator(withKnobs)
|
||||||
|
.add(
|
||||||
|
'Button dialog',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${dialogDemoStyle}
|
||||||
|
</style>
|
||||||
|
<p>
|
||||||
|
Important note: Your <code>slot="content"</code> gets moved to global overlay container.
|
||||||
|
After initialization it is no longer a child of <code>lion-dialog</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To close your dialog from some action performed inside the content slot, fire a
|
||||||
|
<code>close</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)
|
||||||
|
</p>
|
||||||
|
<p>The demo below demonstrates this</p>
|
||||||
|
<div class="demo-box">
|
||||||
|
<lion-dialog>
|
||||||
|
<lion-button slot="invoker">Dialog</lion-button>
|
||||||
|
<div slot="content" class="dialog">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Custom configuration', () => {
|
||||||
|
const dialog = placement => html`
|
||||||
|
<lion-dialog .config=${{ viewportConfig: { placement } }}>
|
||||||
|
<lion-button slot="invoker">Dialog ${placement}</lion-button>
|
||||||
|
<div slot="content" class="dialog">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${dialogDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
${dialog('center')} ${dialog('top-left')} ${dialog('top-right')} ${dialog('bottom-left')}
|
||||||
|
${dialog('bottom-right')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.add('Toggle placement with knobs', () => {
|
||||||
|
const dialog = html`
|
||||||
|
<lion-dialog .config=${object('config', { viewportConfig: { placement: 'center' } })}>
|
||||||
|
<lion-button slot="invoker">Dialog</lion-button>
|
||||||
|
<div slot="content" class="dialog">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${dialogDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
${dialog}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
75
packages/dialog/test/lion-dialog.test.js
Normal file
75
packages/dialog/test/lion-dialog.test.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
</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('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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -204,6 +204,7 @@ 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() {
|
||||||
return html`
|
return html`
|
||||||
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}>
|
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}>
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,10 @@ export const LocalizeMixin = dedupeMixin(
|
||||||
if (this.__localizeMessageSync) {
|
if (this.__localizeMessageSync) {
|
||||||
return localize.msg(...args);
|
return localize.msg(...args);
|
||||||
}
|
}
|
||||||
return until(this.localizeNamespacesLoaded.then(() => localize.msg(...args)), nothing);
|
return until(
|
||||||
|
this.localizeNamespacesLoaded.then(() => localize.msg(...args)),
|
||||||
|
nothing,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
__getUniqueNamespaces() {
|
__getUniqueNamespaces() {
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,26 @@
|
||||||
|
|
||||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||||
|
|
||||||
Supports different types of overlays like dialogs, toasts, tooltips, dropdown, etc...
|
> 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.
|
Manages their position on the screen relative to other elements, including other overlays.
|
||||||
|
|
||||||
|
Exports `lion-overlay`, which is a generic component wrapping OverlayController.
|
||||||
|
Its purpose is to make it easy to use our Overlay System declaratively. It can be easily extended where needed, to override event listeners and more.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [**Overlays Manager**](./docs/OverlaysManager.md), a global repository keeping track of all different types of overlays.
|
- lion-overlay web component:
|
||||||
- [**Overlays System: Scope**](./docs/OverlaySystemScope.md), outline of all possible occurrences of overlays. Divided into two main types:
|
|
||||||
- [**Global Overlay Controller**](./docs/GlobalOverlayController.md), controller for overlays relative to the viewport.
|
- Show content when clicking the invoker
|
||||||
- [**Local Overlay Controller**](./docs/LocalOverlayController.md), controller for overlays positioned next to invokers they are related to.
|
- 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
|
||||||
|
- [**OverlayController**](./docs/OverlayController.md), a single controller class for handling overlays
|
||||||
|
- **OverlayMixin**, a mixin that can be used to create webcomponents that use the OverlayController under the hood
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
|
|
@ -23,18 +34,43 @@ npm i --save @lion/overlays
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { overlays } from '@lion/overlays';
|
import '@lion/overlays/lion-overlay.js';
|
||||||
|
|
||||||
const myCtrl = overlays.add(
|
html`
|
||||||
new OverlayTypeController({
|
<lion-overlay>
|
||||||
/* options */
|
<div slot="content" class="tooltip" .config=${{
|
||||||
}),
|
placementMode: global,
|
||||||
);
|
viewportConfig: { placement: 'bottom-right' },
|
||||||
// name OverlayTypeController is for illustration purpose only
|
}}>
|
||||||
// please read below about existing classes for different types of overlays
|
This is an overlay
|
||||||
|
<button
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('overlay-close', { bubbles: true }))}
|
||||||
|
>x</button>
|
||||||
|
<div>
|
||||||
|
<button slot="invoker">
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</lion-overlay>
|
||||||
|
`;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rationals
|
Or by creating a controller yourself
|
||||||
|
|
||||||
- No `aria-controls`: as support for it is not quite there yet
|
```js
|
||||||
- No `aria-haspopup` People knowing the haspop up and hear about it don’t expect a dialog to open (at this moment in time) but expect a sub-menu. Until support for the dialog value has better implementation, it’s probably best to not use aria-haspopup on the element that opens the modal dialog.
|
import { OverlayController } from '@lion/overlays';
|
||||||
|
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withModalDialogConfig(),
|
||||||
|
invokerNode,
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rationales
|
||||||
|
|
||||||
|
For rationales, please check the [docs](./docs) folder, where we go more in-depth.
|
||||||
|
|
||||||
|
### Aria roles
|
||||||
|
|
||||||
|
- No `aria-controls` as support for it is not quite there yet
|
||||||
|
- No `aria-haspopup`. People knowing the haspop up and hear about it don’t expect a dialog to open (at this moment in time) but expect a sub-menu. Until support for the dialog value has better implementation, it’s probably best to not use aria-haspopup on the element that opens the modal dialog.
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
# GlobalOverlayController
|
|
||||||
|
|
||||||
This is a base class for different global overlays (e.g. a dialog, see [Overlay System: Scope](./OverlaySystemScope.md) - the ones positioned relatively to the viewport).
|
|
||||||
|
|
||||||
You should not use this controller directly unless you want to create a unique type of global overlays which is not supported out of the box. But for implementation details check out [Overlay System: Implementation](./OverlaySystemImplementation.md).
|
|
||||||
|
|
||||||
All supported types of global overlays are described below.
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i --save @lion/overlays
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { overlays } from '@lion/overlays';
|
|
||||||
|
|
||||||
const myCtrl = overlays.add(
|
|
||||||
new GlobalOverlayController({
|
|
||||||
/* options */
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### BottomSheetController
|
|
||||||
|
|
||||||
A specific extension of GlobalOverlayController configured to create accessible dialogs at the bottom of the screen.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { BottomSheetController } from '@lion/overlays';
|
|
||||||
```
|
|
||||||
|
|
||||||
### ModalDialogController
|
|
||||||
|
|
||||||
A specific extension of GlobalOverlayController configured to create accessible modal dialogs placed in the center of the screen.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { ModalDialogController } from '@lion/overlays';
|
|
||||||
```
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
# LocalOverlayController
|
|
||||||
|
|
||||||
This is a base class for different local overlays (e.g. a [tooltip](../../tooltip/), see [Overlay System: Scope](./OverlaySystemScope.md) - the ones positioned next to invokers they are related to).
|
|
||||||
|
|
||||||
For more information strictly about the positioning of the content element to the reference element (invoker), please refer to the [positioning documentation](./LocalOverlayPositioning.md).
|
|
||||||
|
|
||||||
You should not use this controller directly unless you want to create a unique type of local overlays which is not supported out of the box. But for implementation details check out [Overlay System: Implementation](./OverlaySystemImplementation.md).
|
|
||||||
|
|
||||||
All supported types of local overlays are described below.
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i --save @lion/overlays
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { overlays } from '@lion/overlays';
|
|
||||||
|
|
||||||
const myCtrl = overlays.add(
|
|
||||||
new LocalOverlayController({
|
|
||||||
/* options */
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
This is currently WIP.
|
|
||||||
Stay tuned for updates on new types of overlays.
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
# LocalOverlayPositioning
|
|
||||||
|
|
||||||
## Featuring - [Popper.js](https://popper.js.org/)
|
|
||||||
|
|
||||||
Our local overlays use the open-source Popper.js library for positioning the content relative to the reference element, which we usually refer to as the invoker, in the context of local overlays.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Everything Popper.js!
|
|
||||||
- Currently eagerly loads popper in the constructor of LocalOverlayController. Loading during idle time / using prefetch would be better, this is still WIP.
|
|
||||||
|
|
||||||
> Popper strictly is scoped on positioning. **It does not change the dimensions of the popper element nor the reference element**. This also means that if you use the arrow feature, you are in charge of styling it properly, use the x-placement attribute for this.
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
For installation, see [LocalOverlayController](./LocalOverlayController.md)'s `How to use` section.
|
|
||||||
|
|
||||||
The API for LocalOverlay without Popper looks like this (`overlays` being the OverlayManager singleton):
|
|
||||||
|
|
||||||
```js
|
|
||||||
const localOverlay = overlays.add(
|
|
||||||
new LocalOverlayController({
|
|
||||||
contentTemplate: () =>
|
|
||||||
html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`,
|
|
||||||
invokerTemplate: () =>
|
|
||||||
html`
|
|
||||||
<button @click=${() => popupController.toggle()}>UK</button>
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
This will use the defaults we set for Popper configuration. To override the default options, you add a `popperConfig` object to the properties of the object you pass to `the LocalOverlayController` like so:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const localOverlay = overlays.add(
|
|
||||||
new LocalOverlayController({
|
|
||||||
contentTemplate: () =>
|
|
||||||
html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`,
|
|
||||||
invokerTemplate: () =>
|
|
||||||
html`
|
|
||||||
<button @click=${() => popupController.toggle()}>UK</button>
|
|
||||||
`,
|
|
||||||
popperConfig: {
|
|
||||||
/* Placement of popper element, relative to reference element */
|
|
||||||
placement: 'bottom-start',
|
|
||||||
positionFixed: true,
|
|
||||||
modifiers: {
|
|
||||||
/* Prevents detachment of content element from reference element */
|
|
||||||
keepTogether: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
/* When enabled, adds shifting/sliding behavior on secondary axis */
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: false,
|
|
||||||
boundariesElement: 'viewport',
|
|
||||||
/* When enabled, this is the <boundariesElement>-margin for the secondary axis */
|
|
||||||
padding: 32,
|
|
||||||
},
|
|
||||||
/* Use to adjust flipping behavior or constrain directions */
|
|
||||||
flip: {
|
|
||||||
boundariesElement: 'viewport',
|
|
||||||
/* <boundariesElement>-margin for flipping on primary axis */
|
|
||||||
padding: 16,
|
|
||||||
},
|
|
||||||
/* When enabled, adds an offset to either primary or secondary axis */
|
|
||||||
offset: {
|
|
||||||
enabled: true,
|
|
||||||
/* margin between popper and referenceElement */
|
|
||||||
offset: `0, 16px`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The popperConfig is 1 to 1 aligned with Popper.js' API. For more detailed information and more advanced options, visit the [Popper.js documentation](https://popper.js.org/popper-documentation.html) to learn about the usage.
|
|
||||||
|
|
||||||
## Future additions
|
|
||||||
|
|
||||||
- Coming soon: Webcomponent implementation of LocalOverlay with a default arrow, styled out of the box to at least have proper rotations and positions.
|
|
||||||
- Default overflow and/or max-width behavior when content is too wide or high for the viewport.
|
|
||||||
189
packages/overlays/docs/OverlayController.md
Normal file
189
packages/overlays/docs/OverlayController.md
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
# Overlay System
|
||||||
|
|
||||||
|
This document provides an outline of all possible occurrences of overlays found in applications in general and thus provided by Lion. For all concepts referred to in this document, please read [Overlay System Scope](./OverlaySystemScope.md), which includes more background knowledge on overlays on the web.
|
||||||
|
|
||||||
|
OverlayController is the single class we instantiate whenever creating an overlay instance.
|
||||||
|
Based on provided config, it will handle:
|
||||||
|
|
||||||
|
- DOM position (local vs global)
|
||||||
|
- positioning logic
|
||||||
|
- accessibility
|
||||||
|
- interaction patterns.
|
||||||
|
|
||||||
|
and has the following public functions:
|
||||||
|
|
||||||
|
- **show()**, to show the overlay.
|
||||||
|
- **hide()**, to hide the overlay.
|
||||||
|
- **toggle()**, to toggle between show and hide.
|
||||||
|
|
||||||
|
All overlays contain an invokerNode and a contentNode
|
||||||
|
|
||||||
|
- **contentNode**, the toggleable content of the overlay
|
||||||
|
- **invokerNode**, the element toggles the visibility of the content. For local overlays, this is the relative element the content is positioned to.
|
||||||
|
|
||||||
|
For DOM position, local refers to overlays where the content is positioned next to the invokers they are related to, DOM-wise.
|
||||||
|
Global refers to overlays where the content is positioned in a global root node at the bottom of `<body>`.
|
||||||
|
|
||||||
|
## Configuration options
|
||||||
|
|
||||||
|
In total, we should end up with configuration options as depicted below, for all possible overlays.
|
||||||
|
All boolean flags default to 'false'.
|
||||||
|
|
||||||
|
```text
|
||||||
|
- {Boolean} trapsKeyboardFocus - rotates tab.
|
||||||
|
- {Boolean} hidesOnEsc - hides the overlay when pressing [esc].
|
||||||
|
- {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.
|
||||||
|
- {Boolean} preventsScroll - prevents scrolling body content when overlay opened.
|
||||||
|
- {Object} viewportConfig - placementMode: local only
|
||||||
|
- {String} placement: 'top-left' | 'top' | 'top-right' | 'right' | 'bottom-left' |'bottom' | 'bottom-right' | 'left' | 'center'
|
||||||
|
- {Object} popperConfig - placementMode: local only
|
||||||
|
- {String} placement: 'top-left' | 'top' | 'top-right' | 'right' | 'bottom-left' |'bottom' | 'bottom-right' | 'left' | 'center'
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: popperConfig reflects [Popper.js API](https://popper.js.org/popper-documentation.html)
|
||||||
|
|
||||||
|
## Specific Controllers
|
||||||
|
|
||||||
|
You can find our existing configurations [here](../src/configurations):
|
||||||
|
|
||||||
|
- withModalDialogConfig
|
||||||
|
- withDropdownConfig
|
||||||
|
- withBottomSheetConfig
|
||||||
|
|
||||||
|
You import these using ES Modules, and then simply call them inside your OverlayController instantiation:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withModalDialogConfig(),
|
||||||
|
invokerNode,
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsive switching
|
||||||
|
|
||||||
|
Currently we support switching between overlay configurations. Keep in mind however that we do not yet support switching between overlay configurations while the content is shown. If you try, it will close the content if it is open, and the user will need to re-open. Will be supported in the near future.
|
||||||
|
|
||||||
|
What follows is an example implementation on an `OverlayController` instance which checks the viewport width, and then updates the configuration to a bottom sheet versus a modal dialog on `before-show`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
myOverlayCtrl.addEventListener('before-show', () => {
|
||||||
|
if (window.innerWidth >= 600) {
|
||||||
|
ctrl.updateConfig(withModalDialogConfig());
|
||||||
|
} else {
|
||||||
|
ctrl.updateConfig(withBottomSheetConfig());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
An example implementation inside of a webcomponent that uses the `OverlayMixin`:
|
||||||
|
Overriding protected method `_defineOverlay`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
_defineOverlay({ invokerNode, contentNode }) {
|
||||||
|
|
||||||
|
// initial
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withBottomSheetConfig(),
|
||||||
|
hidesOnOutsideClick: true,
|
||||||
|
invokerNode,
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
// responsive
|
||||||
|
ctrl.addEventListener('before-show', () => {
|
||||||
|
if (window.innerWidth >= 600) {
|
||||||
|
ctrl.updateConfig(withModalDialogConfig());
|
||||||
|
} else {
|
||||||
|
ctrl.updateConfig(withBottomSheetConfig());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ctrl;
|
||||||
|
```
|
||||||
|
|
||||||
|
We do not yet support a way to add responsive switching behavior declaratively inside your lit-templates, for our existing overlay webcomponents (e.g. `lion-dialog`). Your best bet for now would be to extend it and only override `_defineOverlay` to include a `before-show` handler as mentioned above.
|
||||||
|
|
||||||
|
## popperConfig for local overlays (placementMode: local)
|
||||||
|
|
||||||
|
> In Popper, content node is often referred to as Popper element, and invoker node is often referred to as the reference element.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
|
||||||
|
- Everything Popper features!
|
||||||
|
- Currently eagerly loads popper if mode is local, in the constructor. Loading during idle time / using prefetch would be better, this is still WIP. PRs are welcome!
|
||||||
|
|
||||||
|
> Popper strictly is scoped on positioning. **It does not change the dimensions of the content node nor the invoker node**. This also means that if you use the arrow feature, you are in charge of styling it properly, use the x-placement attribute for this.
|
||||||
|
|
||||||
|
To override the default options we set for local mode, you add a `popperConfig` object to the config passed to the OverlayController.
|
||||||
|
Here's a succinct overview of some often used popper properties:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const overlayCtrl = new OverlayController({
|
||||||
|
contentNode,
|
||||||
|
invokerNode,
|
||||||
|
popperConfig: {
|
||||||
|
/* Placement of content node, relative to invoker node */
|
||||||
|
placement: 'bottom-start',
|
||||||
|
positionFixed: true,
|
||||||
|
modifiers: {
|
||||||
|
/* Prevents detachment of content node from invoker node */
|
||||||
|
keepTogether: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
/* When enabled, adds shifting/sliding behavior on secondary axis */
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: false,
|
||||||
|
boundariesElement: 'viewport',
|
||||||
|
/* When enabled, this is the <boundariesElement>-margin for the secondary axis */
|
||||||
|
padding: 32,
|
||||||
|
},
|
||||||
|
/* Use to adjust flipping behavior or constrain directions */
|
||||||
|
flip: {
|
||||||
|
boundariesElement: 'viewport',
|
||||||
|
/* <boundariesElement>-margin for flipping on primary axis */
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
/* When enabled, adds an offset to either primary or secondary axis */
|
||||||
|
offset: {
|
||||||
|
enabled: true,
|
||||||
|
/* margin between content node and invoker node */
|
||||||
|
offset: `0, 16px`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future
|
||||||
|
|
||||||
|
### Potential example implementations for overlays
|
||||||
|
|
||||||
|
- Combobox/autocomplete Component
|
||||||
|
- Application menu Component
|
||||||
|
- Popover Component
|
||||||
|
- Dropdown Component
|
||||||
|
- Toast Component
|
||||||
|
|
||||||
|
### Potential configuration additions
|
||||||
|
|
||||||
|
```text
|
||||||
|
- {Boolean} isModal - sets [aria-modal] and/or [aria-hidden="true"] on siblings
|
||||||
|
- {Boolean} isTooltip - has a totally different interaction - and accessibility pattern from all other overlays, so needed for internals.
|
||||||
|
- {Boolean} handlesUserInteraction - sets toggle on click, or hover when `isTooltip`
|
||||||
|
- {Boolean} handlesAccessibility -
|
||||||
|
- For non `isTooltip`:
|
||||||
|
- sets [aria-expanded="true/false"] and [aria-haspopup="true"] on invokerNode
|
||||||
|
- sets [aria-controls] on invokerNode
|
||||||
|
- returns focus to invokerNode on hide
|
||||||
|
- sets focus to overlay content(?)
|
||||||
|
- For `isTooltip`:
|
||||||
|
- sets [role="tooltip"] and [aria-labelledby]/[aria-describedby] on the content
|
||||||
|
```
|
||||||
|
|
||||||
|
### Future for mode local (Popper)
|
||||||
|
|
||||||
|
- Coming soon: Webcomponent implementation of LocalOverlay with a default arrow, styled out of the box to at least have proper rotations and positions.
|
||||||
|
- Default overflow and/or max-width behavior when content is too wide or high for the viewport.
|
||||||
|
|
@ -1,244 +0,0 @@
|
||||||
# Overlay System: Implementation
|
|
||||||
|
|
||||||
This document provides an outline of all possible occurrences of overlays found in applications in general and thus provided by Lion. For all concepts referred to in this document, please read [Overlay System Scope](./OverlaySystemScope.md).
|
|
||||||
|
|
||||||
## Base controller
|
|
||||||
|
|
||||||
The BaseController handles the basics of all controllers, and has the following public functions:
|
|
||||||
|
|
||||||
- **show()**, to show the overlay.
|
|
||||||
- **hide()**, to hide the overlay.
|
|
||||||
- **toggle()**, to toggle between show and hide.
|
|
||||||
|
|
||||||
All overlays exists of an invoker and a content
|
|
||||||
|
|
||||||
- **invoker**, the element that can trigger showing (and hiding) the overlay.
|
|
||||||
- invokerNode
|
|
||||||
- **content**, the toggleable overlays content
|
|
||||||
- contentTemplate, in most cases the content will be placed inside a template as one of the controller configuration options.
|
|
||||||
- contentNode, a node can also be used as the content for local overlays (see next section), such as is done in the [popup](../../popup/).
|
|
||||||
|
|
||||||
## Local and global overlay controllers
|
|
||||||
|
|
||||||
Currently, we have a global and a local overlay controller, as two separate entities.
|
|
||||||
Based on provided config, they handle all positioning logic, accessibility and interaction patterns.
|
|
||||||
|
|
||||||
- [GlobalOverlayController](./GlobalOverlayController.md), the ones positioned relatively to the viewport.
|
|
||||||
- [LocalOverlayController](./LocalOverlayController.md), the ones positioned next to invokers they are related to.
|
|
||||||
|
|
||||||
All of their configuration options will be described below as part of the _Configuration options_ section.
|
|
||||||
|
|
||||||
### DynamicOverlayController
|
|
||||||
|
|
||||||
Based on screen size, we might want to switch the appearance of an overlay.
|
|
||||||
For instance: an application menu can be displayed as a dropdown on desktop,
|
|
||||||
but as a bottom sheet on mobile.
|
|
||||||
|
|
||||||
Similarly, a dialog can be displayed as a popover on desktop, but as a (global) dialog on mobile.
|
|
||||||
|
|
||||||
The DynamicOverlayController is a flexible overlay that can switch between different controllers, also between the connection point in dom (global and local). The switch is only done when the overlay is closed, so the focus isn't lost while switching from one overlay to another.
|
|
||||||
|
|
||||||
### Configuration options
|
|
||||||
|
|
||||||
In total, we should end up with configuration options as depicted below, for all possible overlays.
|
|
||||||
All boolean flags default to 'false'.
|
|
||||||
Some options are mutually exclusive, in which case their dependent options and requirement will be mentioned.
|
|
||||||
|
|
||||||
> Note: a more generic and precise term for all mentionings of `invoker` below would actually be `relative positioning element`.
|
|
||||||
|
|
||||||
#### Shared configuration options
|
|
||||||
|
|
||||||
```text
|
|
||||||
- {Boolean} trapsKeyboardFocus - rotates tab, implicitly set when 'isModal'.
|
|
||||||
- {Boolean} hidesOnEsc - hides the overlay when pressing [esc].
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Global specific configuration options
|
|
||||||
|
|
||||||
```text
|
|
||||||
- {Element} elementToFocusAfterHide - the element that should be called `.focus()` on after dialog closes.
|
|
||||||
- {Boolean} hasBackdrop - whether it should have a backdrop.
|
|
||||||
- {Boolean} isBlocking - hides other overlays when multiple are opened.
|
|
||||||
- {Boolean} preventsScroll - prevents scrolling body content when overlay opened.
|
|
||||||
- {Object} viewportConfig
|
|
||||||
- {String} placement: 'top-left' | 'top' | 'top-right' | 'right' | 'bottom-left' |'bottom' |'bottom-right' |'left' | 'center'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Local specific configuration options
|
|
||||||
|
|
||||||
```text
|
|
||||||
- {Boolean} hidesOnOutsideClick - hides the overlay when clicking next to it, excluding invoker.
|
|
||||||
- {String} cssPosition - 'absolute' or 'fixed'. TODO: choose name that cannot be mistaken for placement like cssPosition or positioningTechnique: <https://github.com/ing-bank/lion/pull/61>.
|
|
||||||
- For positioning checkout [localOverlayPositioning](./localOverlayPositioning.md).
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Suggested additions
|
|
||||||
|
|
||||||
```text
|
|
||||||
- {Boolean} isModal - sets [aria-modal] and/or [aria-hidden="true"] on siblings
|
|
||||||
- {Boolean} isTooltip - has a totally different interaction - and accessibility pattern from all other overlays, so needed for internals.
|
|
||||||
- {Boolean} handlesUserInteraction - sets toggle on click, or hover when `isTooltip`
|
|
||||||
- {Boolean} handlesAccessibility -
|
|
||||||
- For non `isTooltip`:
|
|
||||||
- sets [aria-expanded="true/false"] and [aria-haspopup="true"] on invokerNode
|
|
||||||
- sets [aria-controls] on invokerNode
|
|
||||||
- returns focus to invokerNode on hide
|
|
||||||
- sets focus to overlay content(?)
|
|
||||||
- For `isTooltip`:
|
|
||||||
- sets [role="tooltip"] and [aria-labelledby]/[aria-describedby] on the content
|
|
||||||
```
|
|
||||||
|
|
||||||
## Specific Controllers
|
|
||||||
|
|
||||||
Controllers/behaviors provide preconfigured configuration objects for the global/local
|
|
||||||
overlay controllers.
|
|
||||||
|
|
||||||
They provide an imperative and very flexible api for creating overlays and should be used by
|
|
||||||
Subclassers, inside webcomponents.
|
|
||||||
|
|
||||||
### Dialog Controller
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
isModal: true,
|
|
||||||
hasBackdrop: true,
|
|
||||||
preventsScroll: true,
|
|
||||||
trapsKeyboardFocus: true,
|
|
||||||
hidesOnEsc: true,
|
|
||||||
handlesUserInteraction: true,
|
|
||||||
handlesAccessibility: true,
|
|
||||||
viewportConfig: {
|
|
||||||
placement: 'center',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tooltip Controller
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
isTooltip: true,
|
|
||||||
handlesUserInteraction: true,
|
|
||||||
handlesAccessibility: true,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Popover Controller
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
handlesUserInteraction: true,
|
|
||||||
handlesAccessibility: true,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dropdown Controller
|
|
||||||
|
|
||||||
It will be quite common to override placement to 'bottom-fullwidth'.
|
|
||||||
Also, it would be quite common to add a pointerNode.
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
placement: 'bottom',
|
|
||||||
handlesUserInteraction: true,
|
|
||||||
handlesAccessibility: true,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Toast Controller
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
- add an option for role="alertdialog" ?
|
|
||||||
- add an option for a 'hide timer' and belonging a11y features for this
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
viewportconfig: {
|
|
||||||
placement: 'top-right',
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
### BottomSheetController
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
viewportConfig: {
|
|
||||||
placement: 'bottom',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Select Controller
|
|
||||||
|
|
||||||
No need for a config, will probably invoke ResponsiveOverlayCtrl and switches
|
|
||||||
config based on media query from Dropdown to BottomSheet/CenteredDialog
|
|
||||||
|
|
||||||
### Combobox/autocomplete Controller
|
|
||||||
|
|
||||||
No need for a config, will probably invoke ResponsiveOverlayCtrl and switches
|
|
||||||
config based on media query from Dropdown to BottomSheet/CenteredDialog
|
|
||||||
|
|
||||||
### Application menu Controller
|
|
||||||
|
|
||||||
No need for cfg, will probably invoke ResponsiveOverlayCtrl and switches
|
|
||||||
config based on media query from Dropdown to BottomSheet/CenteredDialog
|
|
||||||
|
|
||||||
## Web components
|
|
||||||
|
|
||||||
Web components provide a declarative, developer friendly interface with a preconfigured styling that fits the Design System and makes it really easy for Application Developers to build user interfaces.
|
|
||||||
|
|
||||||
Web components should use the ground layers for the webcomponents in Lion are the following:
|
|
||||||
|
|
||||||
### Dialog Component
|
|
||||||
|
|
||||||
Imperative might be better here? We can add a web component later if needed.
|
|
||||||
|
|
||||||
### Tooltip Component
|
|
||||||
|
|
||||||
```html
|
|
||||||
<lion-tooltip>
|
|
||||||
<button slot="invoker">hover/focus</button>
|
|
||||||
<div slot="content">This will be shown</div>
|
|
||||||
</lion-tooltip>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Popover Component
|
|
||||||
|
|
||||||
```html
|
|
||||||
<lion-popover>
|
|
||||||
<button slot="invoker">click/space/enter</button>
|
|
||||||
<div slot="content">This will be shown</div>
|
|
||||||
</lion-popover>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Dropdown Component
|
|
||||||
|
|
||||||
Like the name suggests, the default placement will be bottom
|
|
||||||
|
|
||||||
```html
|
|
||||||
<lion-dropdown>
|
|
||||||
<button slot="invoker">click/space/enter</button>
|
|
||||||
<ul slot="content">
|
|
||||||
<li>This</li>
|
|
||||||
<li>will be</li>
|
|
||||||
<li>shown</li>
|
|
||||||
</ul>
|
|
||||||
</lion-dropdown>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Toast Component
|
|
||||||
|
|
||||||
Imperative might be better here?
|
|
||||||
|
|
||||||
### Sheet Component (bottom, top, left, right)
|
|
||||||
|
|
||||||
Imperative might be better here?
|
|
||||||
|
|
||||||
## Web components implementing generic overlays
|
|
||||||
|
|
||||||
### Select, Combobox/autocomplete, Application menu
|
|
||||||
|
|
||||||
Those will be separate web components with a lot of form and a11y logic that will be described in detail in different sections.
|
|
||||||
|
|
||||||
They will implement the Overlay configuration as described above under 'Controllers/behaviors'.
|
|
||||||
|
|
@ -17,7 +17,7 @@ As opposed to a single overlay, the overlay manager stores knowledge about:
|
||||||
The manager is in charge of rendering an overlay to the DOM. Therefore, a developer should be able
|
The manager is in charge of rendering an overlay to the DOM. Therefore, a developer should be able
|
||||||
to control:
|
to control:
|
||||||
|
|
||||||
- It’s ‘physical position’ (where the dialog is attached). This can either be:
|
- Its ‘physical position’ (where the dialog is attached). This can either be:
|
||||||
- globally: at root level of the DOM. This guarantees a total control over its painting, since
|
- globally: at root level of the DOM. This guarantees a total control over its painting, since
|
||||||
the stacking context can be controlled from here and interfering parents (that set overflow
|
the stacking context can be controlled from here and interfering parents (that set overflow
|
||||||
values or transforms) can’t be apparent. Additionally, making a modal dialog requiring
|
values or transforms) can’t be apparent. Additionally, making a modal dialog requiring
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Overlay Manager
|
# Overlay Manager
|
||||||
|
|
||||||
An overlay manager is a global repository keeping track of all different types of overlays. The need for a global housekeeping mainly arises when multiple overlays are opened simultaneously.
|
An overlay manager is a global registry keeping track of all different types of overlays. The need for a global housekeeping mainly arises when multiple overlays are opened simultaneously.
|
||||||
|
|
||||||
The overlay manager keeps track of all registered overlays and controls which one to show.
|
The overlay manager keeps track of all registered overlays and controls which one to show.
|
||||||
|
|
|
||||||
141
packages/overlays/docs/migration.md
Normal file
141
packages/overlays/docs/migration.md
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
# 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('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>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
@ -7,3 +7,5 @@ export { OverlayMixin } from './src/OverlayMixin.js';
|
||||||
export { withBottomSheetConfig } from './src/configurations/withBottomSheetConfig.js';
|
export { withBottomSheetConfig } from './src/configurations/withBottomSheetConfig.js';
|
||||||
export { withModalDialogConfig } from './src/configurations/withModalDialogConfig.js';
|
export { withModalDialogConfig } from './src/configurations/withModalDialogConfig.js';
|
||||||
export { withDropdownConfig } from './src/configurations/withDropdownConfig.js';
|
export { withDropdownConfig } from './src/configurations/withDropdownConfig.js';
|
||||||
|
|
||||||
|
export { LionOverlay } from './src/LionOverlay.js';
|
||||||
|
|
|
||||||
3
packages/overlays/lion-overlay.js
Normal file
3
packages/overlays/lion-overlay.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { LionOverlay } from './src/LionOverlay.js';
|
||||||
|
|
||||||
|
customElements.define('lion-overlay', LionOverlay);
|
||||||
|
|
@ -37,6 +37,8 @@
|
||||||
"popper.js": "^1.15.0"
|
"popper.js": "^1.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@lion/button": "^0.3.32",
|
||||||
|
"@lion/icon": "^0.2.8",
|
||||||
"@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",
|
||||||
|
|
|
||||||
95
packages/overlays/src/LionOverlay.js
Normal file
95
packages/overlays/src/LionOverlay.js
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { LitElement, html } from '@lion/core';
|
||||||
|
import { OverlayMixin } from './OverlayMixin.js';
|
||||||
|
import { OverlayController } from './OverlayController.js';
|
||||||
|
|
||||||
|
export class LionOverlay extends OverlayMixin(LitElement) {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get config() {
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
set config(value) {
|
||||||
|
if (this._overlayCtrl) {
|
||||||
|
this._overlayCtrl.updateConfig(value);
|
||||||
|
}
|
||||||
|
this._config = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: This should be refactored to Array.from(this.children).find(child => child.slot === 'content')
|
||||||
|
// When this issue is fixed https://github.com/ing-bank/lion/issues/382
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* Overrides OverlayMixin
|
||||||
|
* Important to use this override, so that later, contentTemplates can also be accepted
|
||||||
|
*/
|
||||||
|
get _overlayContentNode() {
|
||||||
|
const contentNode = this.querySelector('[slot=content]');
|
||||||
|
if (contentNode) {
|
||||||
|
this._cachedOverlayContentNode = contentNode;
|
||||||
|
}
|
||||||
|
return contentNode || this._cachedOverlayContentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* Overrides OverlayMixin
|
||||||
|
*/
|
||||||
|
get _overlayInvokerNode() {
|
||||||
|
return Array.from(this.children).find(child => child.slot === 'invoker');
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_defineOverlay({ contentNode, invokerNode }) {
|
||||||
|
return new OverlayController({
|
||||||
|
placementMode: 'global', // have to set a default
|
||||||
|
contentNode,
|
||||||
|
invokerNode,
|
||||||
|
...this.config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupShowHideListeners() {
|
||||||
|
this.__close = () => {
|
||||||
|
this.opened = false;
|
||||||
|
};
|
||||||
|
this.__toggle = () => {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
};
|
||||||
|
this._overlayCtrl.invokerNode.addEventListener('click', this.__toggle);
|
||||||
|
this._overlayCtrl.contentNode.addEventListener('close', this.__close);
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownShowHideListeners() {
|
||||||
|
this._overlayCtrl.invokerNode.removeEventListener('click', this.__toggle);
|
||||||
|
this._overlayCtrl.contentNode.removeEventListener('close', this.__close);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._setupShowHideListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._teardownShowHideListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ export class OverlayController {
|
||||||
contentNode: config.contentNode,
|
contentNode: config.contentNode,
|
||||||
invokerNode: config.invokerNode,
|
invokerNode: config.invokerNode,
|
||||||
referenceNode: null,
|
referenceNode: null,
|
||||||
elementToFocusAfterHide: document.body,
|
elementToFocusAfterHide: config.invokerNode,
|
||||||
inheritsReferenceWidth: '',
|
inheritsReferenceWidth: '',
|
||||||
hasBackdrop: false,
|
hasBackdrop: false,
|
||||||
isBlocking: false,
|
isBlocking: false,
|
||||||
|
|
@ -103,6 +103,18 @@ export class OverlayController {
|
||||||
* @param {OverlayConfig} cfgToAdd
|
* @param {OverlayConfig} cfgToAdd
|
||||||
*/
|
*/
|
||||||
updateConfig(cfgToAdd) {
|
updateConfig(cfgToAdd) {
|
||||||
|
// only updating the viewportConfig
|
||||||
|
if (Object.keys(cfgToAdd).length === 1 && Object.keys(cfgToAdd)[0] === 'viewportConfig') {
|
||||||
|
this.updateViewportConfig(cfgToAdd.viewportConfig);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only updating the popperConfig
|
||||||
|
if (Object.keys(cfgToAdd).length === 1 && Object.keys(cfgToAdd)[0] === 'popperConfig') {
|
||||||
|
this.updatePopperConfig(cfgToAdd.popperConfig);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Teardown all previous configs
|
// Teardown all previous configs
|
||||||
this._handleFeatures({ phase: 'teardown' });
|
this._handleFeatures({ phase: 'teardown' });
|
||||||
|
|
||||||
|
|
@ -167,6 +179,7 @@ export class OverlayController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: Consider that state can also be shown (rather than only initial/closed), and don't hide in that case
|
||||||
/**
|
/**
|
||||||
* @desc Cleanup ._contentNodeWrapper. We do this, because creating a fresh wrapper
|
* @desc Cleanup ._contentNodeWrapper. We do this, because creating a fresh wrapper
|
||||||
* can lead to problems with event listeners...
|
* can lead to problems with event listeners...
|
||||||
|
|
@ -298,7 +311,10 @@ export class OverlayController {
|
||||||
// We only are allowed to move focus if we (still) 'own' it.
|
// We only are allowed to move focus if we (still) 'own' it.
|
||||||
// Otherwise we assume the 'outside world' has, purposefully, taken over
|
// Otherwise we assume the 'outside world' has, purposefully, taken over
|
||||||
// if (this._contentNodeWrapper.activeElement) {
|
// if (this._contentNodeWrapper.activeElement) {
|
||||||
this.elementToFocusAfterHide.focus();
|
if (this.elementToFocusAfterHide) {
|
||||||
|
console.log(this.elementToFocusAfterHide);
|
||||||
|
this.elementToFocusAfterHide.focus();
|
||||||
|
}
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -550,6 +566,12 @@ export class OverlayController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateViewportConfig(newConfig) {
|
||||||
|
this._handlePosition({ phase: 'hide' });
|
||||||
|
this.viewportConfig = newConfig;
|
||||||
|
this._handlePosition({ phase: 'show' });
|
||||||
|
}
|
||||||
|
|
||||||
teardown() {
|
teardown() {
|
||||||
this._handleFeatures({ phase: 'teardown' });
|
this._handleFeatures({ phase: 'teardown' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
|
||||||
|
|
||||||
import { css } from '@lion/core';
|
|
||||||
import { fixtureSync } from '@open-wc/testing-helpers';
|
|
||||||
import { OverlayController, withBottomSheetConfig } from '../index.js';
|
|
||||||
|
|
||||||
const bottomSheetDemoStyle = css`
|
|
||||||
.demo-overlay {
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid lightgrey;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
storiesOf('Global Overlay System|BottomSheet', module).add('Default', () => {
|
|
||||||
const bottomSheetCtrl = new OverlayController({
|
|
||||||
...withBottomSheetConfig(),
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-overlay">
|
|
||||||
<p>BottomSheet</p>
|
|
||||||
<button @click="${() => bottomSheetCtrl.hide()}">Close</button>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${bottomSheetDemoStyle}
|
|
||||||
</style>
|
|
||||||
<a href="#">Anchor 1</a>
|
|
||||||
<button
|
|
||||||
@click="${event => bottomSheetCtrl.show(event.target)}"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
Open dialog
|
|
||||||
</button>
|
|
||||||
<a href="#">Anchor 2</a>
|
|
||||||
${Array(50).fill(
|
|
||||||
html`
|
|
||||||
<p>Lorem ipsum</p>
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
|
||||||
import { fixtureSync } from '@open-wc/testing-helpers';
|
|
||||||
|
|
||||||
import { css } from '@lion/core';
|
|
||||||
import {
|
|
||||||
OverlayController,
|
|
||||||
withBottomSheetConfig,
|
|
||||||
withModalDialogConfig,
|
|
||||||
withDropdownConfig,
|
|
||||||
} from '../index.js';
|
|
||||||
|
|
||||||
const dynamicOverlayDemoStyle = css`
|
|
||||||
.demo-overlay {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
background-color: white;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-overlay__global--small {
|
|
||||||
height: 100px;
|
|
||||||
width: 100px;
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-overlay__global--big {
|
|
||||||
left: 50px;
|
|
||||||
top: 30px;
|
|
||||||
width: 200px;
|
|
||||||
max-width: 250px;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.12), 0 6px 6px 0 rgba(0, 0, 0, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-overlay__local {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
max-width: 250px;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.12), 0 6px 6px 0 rgba(0, 0, 0, 0.24);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
storiesOf('Dynamic Overlay System| Switching Overlays', module).add(
|
|
||||||
'Switch overlays configuration',
|
|
||||||
() => {
|
|
||||||
const ctrl = new OverlayController({
|
|
||||||
...withBottomSheetConfig(),
|
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
trapsKeyboardFocus: true,
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @click="${() => ctrl.toggle()}">
|
|
||||||
Invoker
|
|
||||||
</button>
|
|
||||||
`),
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div
|
|
||||||
style="background: #eee;"
|
|
||||||
class="demo-overlay demo-overlay__global demo-overlay__global--small"
|
|
||||||
>
|
|
||||||
Content
|
|
||||||
<button @click="${() => ctrl.hide()}">Close</button>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctrlType = document.createElement('div');
|
|
||||||
function switchTo(type) {
|
|
||||||
ctrlType.innerHTML = type;
|
|
||||||
switch (type) {
|
|
||||||
case 'bottom-sheet':
|
|
||||||
ctrl.updateConfig(withBottomSheetConfig());
|
|
||||||
break;
|
|
||||||
case 'dropdown':
|
|
||||||
ctrl.updateConfig({
|
|
||||||
...withDropdownConfig(),
|
|
||||||
hasBackdrop: false,
|
|
||||||
viewportConfig: null,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ctrl.updateConfig(withModalDialogConfig());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${dynamicOverlayDemoStyle}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
${ctrl.invoker}
|
|
||||||
|
|
||||||
<button @click="${() => switchTo('modal-dialog')}">
|
|
||||||
as modal dialog
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="${() => switchTo('bottom-sheet')}">
|
|
||||||
as bottom sheet
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button @click="${() => switchTo('dropdown')}">
|
|
||||||
as dropdown
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,6 +1,390 @@
|
||||||
import './global-overlay.stories.js';
|
import { storiesOf, html, withKnobs } from '@open-wc/demoing-storybook';
|
||||||
import './modal-dialog.stories.js';
|
import { css, render } from '@lion/core';
|
||||||
import './bottom-sheet.stories.js';
|
import '@lion/icon/lion-icon.js';
|
||||||
import './local-overlay.stories.js';
|
import '@lion/button/lion-button.js';
|
||||||
import './local-overlay-placement.stories.js';
|
import { withBottomSheetConfig, withDropdownConfig, withModalDialogConfig } from '../index.js';
|
||||||
import './dynamic-overlay.stories.js';
|
import '../lion-overlay.js';
|
||||||
|
|
||||||
|
function renderOffline(litHtmlTemplate) {
|
||||||
|
const offlineRenderContainer = document.createElement('div');
|
||||||
|
render(litHtmlTemplate, offlineRenderContainer);
|
||||||
|
return offlineRenderContainer.firstElementChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently toggling while opened doesn't work (see OverlayController)
|
||||||
|
/*
|
||||||
|
let toggledPlacement = 'top';
|
||||||
|
const togglePlacement = popupController => {
|
||||||
|
const placements = [
|
||||||
|
'top-end',
|
||||||
|
'top',
|
||||||
|
'top-start',
|
||||||
|
'right-end',
|
||||||
|
'right',
|
||||||
|
'right-start',
|
||||||
|
'bottom-start',
|
||||||
|
'bottom',
|
||||||
|
'bottom-end',
|
||||||
|
'left-start',
|
||||||
|
'left',
|
||||||
|
'left-end',
|
||||||
|
];
|
||||||
|
toggledPlacement = placements[(placements.indexOf(toggledPlacement) + 1) % placements.length];
|
||||||
|
popupController.updatePopperConfig({ togglePlacement });
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
const overlayDemoStyle = css`
|
||||||
|
.demo-box {
|
||||||
|
width: 200px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid grey;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-box_placements {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 173px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
lion-overlay {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
color: black;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-box__column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-popup {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
storiesOf('Overlay System | Overlay Component', module)
|
||||||
|
.addDecorator(withKnobs)
|
||||||
|
.add(
|
||||||
|
'Default',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<p>
|
||||||
|
Important note: Your <code>slot="content"</code> gets moved to global overlay container.
|
||||||
|
After initialization it is no longer a child of <code>lion-overlay</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To close your overlay from some action performed inside the content slot, fire a
|
||||||
|
<code>close</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)
|
||||||
|
</p>
|
||||||
|
<p>The demo below demonstrates this</p>
|
||||||
|
<div class="demo-box">
|
||||||
|
<lion-overlay>
|
||||||
|
<lion-button slot="invoker">Overlay</lion-button>
|
||||||
|
<div slot="content" class="overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-overlay>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Global placement configuration', () => {
|
||||||
|
const overlay = placement => html`
|
||||||
|
<lion-overlay
|
||||||
|
.config=${{ hasBackdrop: true, trapsKeyboardFocus: true, viewportConfig: { placement } }}
|
||||||
|
>
|
||||||
|
<lion-button slot="invoker">Overlay ${placement}</lion-button>
|
||||||
|
<div slot="content" class="overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-overlay>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
${overlay('center')} ${overlay('top-left')} ${overlay('top-right')}
|
||||||
|
${overlay('bottom-left')} ${overlay('bottom-right')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.add(
|
||||||
|
'Local placementMode',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
<lion-overlay
|
||||||
|
.config=${{ placementMode: 'local', popperConfig: { placement: 'bottom-start' } }}
|
||||||
|
>
|
||||||
|
<lion-button slot="invoker">Overlay</lion-button>
|
||||||
|
<div slot="content" class="overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-overlay>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'Override the popper config',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
The API is aligned with Popper.js, visit their documentation for more information:
|
||||||
|
<a href="https://popper.js.org/popper-documentation.html">Popper.js Docs</a>
|
||||||
|
</div>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
<lion-overlay
|
||||||
|
.config=${{
|
||||||
|
placementMode: 'local',
|
||||||
|
hidesOnEsc: true,
|
||||||
|
hidesOnOutsideClick: true,
|
||||||
|
popperConfig: {
|
||||||
|
placement: 'bottom-start',
|
||||||
|
positionFixed: true,
|
||||||
|
modifiers: {
|
||||||
|
keepTogether: {
|
||||||
|
enabled: true /* Prevents detachment of content element from reference element */,
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true /* disables shifting/sliding behavior on secondary axis */,
|
||||||
|
boundariesElement: 'viewport',
|
||||||
|
padding: 32 /* when enabled, this is the viewport-margin for shifting/sliding on secondary axis */,
|
||||||
|
},
|
||||||
|
hide: {
|
||||||
|
/* must come AFTER preventOverflow option */
|
||||||
|
enabled: false /* disables hiding behavior when reference element is outside of popper boundaries */,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
enabled: true,
|
||||||
|
offset: `0, 16px` /* horizontal and vertical margin (distance between popper and referenceElement) */,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div slot="content" class="demo-popup">United Kingdom</div>
|
||||||
|
<button slot="invoker">
|
||||||
|
UK
|
||||||
|
</button>
|
||||||
|
</lion-overlay>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Switch overlays configuration', () => {
|
||||||
|
const overlay = renderOffline(html`
|
||||||
|
<lion-overlay .config=${{ ...withBottomSheetConfig() }}>
|
||||||
|
<lion-button slot="invoker">Overlay</lion-button>
|
||||||
|
<div slot="content" class="overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-overlay>
|
||||||
|
`);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
@click=${() => {
|
||||||
|
overlay.config = {
|
||||||
|
...withModalDialogConfig(),
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
modal dialog
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click=${() => {
|
||||||
|
overlay.config = {
|
||||||
|
...withBottomSheetConfig(),
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
bottom sheet
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click=${() => {
|
||||||
|
overlay.config = {
|
||||||
|
...withDropdownConfig(),
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
dropdown
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
${overlay}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.add('On hover', () => {
|
||||||
|
const popup = renderOffline(html`
|
||||||
|
<lion-overlay
|
||||||
|
.config=${{
|
||||||
|
placementMode: 'local',
|
||||||
|
hidesOnEsc: true,
|
||||||
|
hidesOnOutsideClick: true,
|
||||||
|
popperConfig: {
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
slot="invoker"
|
||||||
|
@mouseenter=${() => {
|
||||||
|
popup.opened = true;
|
||||||
|
}}
|
||||||
|
@mouseleave=${() => {
|
||||||
|
popup.opened = false;
|
||||||
|
}}
|
||||||
|
>UK</span
|
||||||
|
>
|
||||||
|
<div slot="content" class="overlay">
|
||||||
|
United Kingdom
|
||||||
|
</div>
|
||||||
|
</lion-overlay>
|
||||||
|
`);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
In the beautiful ${popup} the weather is nice.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.add('On an input', () => {
|
||||||
|
const popup = renderOffline(html`
|
||||||
|
<lion-overlay
|
||||||
|
.config=${{
|
||||||
|
placementMode: 'local',
|
||||||
|
elementToFocusAfterHide: null,
|
||||||
|
popperConfig: {
|
||||||
|
placement: 'bottom',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div slot="content" class="demo-popup">United Kingdom</div>
|
||||||
|
<input
|
||||||
|
slot="invoker"
|
||||||
|
id="input"
|
||||||
|
type="text"
|
||||||
|
@click=${e => e.stopImmediatePropagation()}
|
||||||
|
@focusout=${() => {
|
||||||
|
popup.opened = false;
|
||||||
|
}}
|
||||||
|
@focusin=${() => {
|
||||||
|
popup.opened = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</lion-overlay>
|
||||||
|
`);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
<label for="input">Input with a dropdown on focus</label>
|
||||||
|
${popup}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
/* .add('Toggle placement with knobs', () => {
|
||||||
|
const overlay = (placementMode = 'global') => html`
|
||||||
|
<lion-overlay
|
||||||
|
.config=${{
|
||||||
|
placementMode,
|
||||||
|
...(placementMode === 'global'
|
||||||
|
? { viewportConfig: { placement: text('global config', 'center') } }
|
||||||
|
: { popperConfig: { placement: text('local config', 'top-start') } }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<lion-button slot="invoker">Overlay</lion-button>
|
||||||
|
<div slot="content" class="overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<lion-button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}
|
||||||
|
>⨯</lion-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-overlay>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
<p>Local</p>
|
||||||
|
${overlay('local')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
<p>Global</p>
|
||||||
|
${overlay()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}) */
|
||||||
|
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
|
||||||
import { fixtureSync } from '@open-wc/testing-helpers';
|
|
||||||
import { css } from '@lion/core';
|
|
||||||
import { OverlayController } from '../index.js';
|
|
||||||
|
|
||||||
let placement = 'top';
|
|
||||||
const togglePlacement = popupController => {
|
|
||||||
const placements = [
|
|
||||||
'top-end',
|
|
||||||
'top',
|
|
||||||
'top-start',
|
|
||||||
'right-end',
|
|
||||||
'right',
|
|
||||||
'right-start',
|
|
||||||
'bottom-start',
|
|
||||||
'bottom',
|
|
||||||
'bottom-end',
|
|
||||||
'left-start',
|
|
||||||
'left',
|
|
||||||
'left-end',
|
|
||||||
];
|
|
||||||
placement = placements[(placements.indexOf(placement) + 1) % placements.length];
|
|
||||||
popupController.updatePopperConfig({ placement });
|
|
||||||
};
|
|
||||||
|
|
||||||
const popupPlacementDemoStyle = css`
|
|
||||||
.demo-box {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid grey;
|
|
||||||
margin: 120px auto 120px 360px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-popup {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid grey;
|
|
||||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.12), 0 6px 6px 0 rgba(0, 0, 0, 0.24);
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
storiesOf('Local Overlay System|Local Overlay Placement', module)
|
|
||||||
.addParameters({ options: { selectedPanel: 'storybook/actions/actions-panel' } })
|
|
||||||
.add('Preferred placement overlay absolute', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
hidesOnEsc: true,
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @click="${() => popup.toggle()}">UK</button>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupPlacementDemoStyle}
|
|
||||||
</style>
|
|
||||||
<button @click=${() => togglePlacement(popup)}>Toggle placement</button>
|
|
||||||
<div class="demo-box">
|
|
||||||
${popup.invoker}${popup.content}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('Override the popper config', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
hidesOnEsc: true,
|
|
||||||
popperConfig: {
|
|
||||||
placement: 'bottom-start',
|
|
||||||
positionFixed: true,
|
|
||||||
modifiers: {
|
|
||||||
keepTogether: {
|
|
||||||
enabled: true /* Prevents detachment of content element from reference element */,
|
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
enabled: false /* disables shifting/sliding behavior on secondary axis */,
|
|
||||||
boundariesElement: 'viewport',
|
|
||||||
padding: 32 /* when enabled, this is the viewport-margin for shifting/sliding */,
|
|
||||||
},
|
|
||||||
flip: {
|
|
||||||
boundariesElement: 'viewport',
|
|
||||||
padding: 16 /* viewport-margin for flipping on primary axis */,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
enabled: true,
|
|
||||||
offset: `0, 16px` /* horizontal and vertical margin (distance between popper and referenceElement) */,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @click="${() => popup.toggle()}">UK</button>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupPlacementDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div>
|
|
||||||
The API is aligned with Popper.js, visit their documentation for more information:
|
|
||||||
<a href="https://popper.js.org/popper-documentation.html">Popper.js Docs</a>
|
|
||||||
</div>
|
|
||||||
<button @click=${() => togglePlacement(popup)}>Toggle placement</button>
|
|
||||||
<div class="demo-box">
|
|
||||||
${popup.invoker} ${popup.content}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
/* TODO: Add this when we have a feature in place that adds scrollbars / overflow when no space is available */
|
|
||||||
/* .add('Space not available', () => {
|
|
||||||
let popup;
|
|
||||||
const invokerNode = document.createElement('button');
|
|
||||||
invokerNode.innerHTML = 'UK';
|
|
||||||
invokerNode.addEventListener('click', () => popup.toggle());
|
|
||||||
let popup = overlays.add(
|
|
||||||
new LocalOverlayController({
|
|
||||||
hidesOnEsc: true,
|
|
||||||
contentTemplate: () => html`
|
|
||||||
<div class="demo-popup">
|
|
||||||
Toggle the placement of this overlay with the buttons. Since there is not enough space
|
|
||||||
available on the vertical center or the top for this popup, the popup will get
|
|
||||||
displayed on the available space on the bottom. Try dragging the viewport to
|
|
||||||
increase/decrease space see the behavior of this.
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
invokerNode,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupPlacementDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div>
|
|
||||||
<button @click=${() => togglePlacement(popup)}>Toggle placement</button>
|
|
||||||
<button @click=${() => popup.hide()}>Close popup</button>
|
|
||||||
</div>
|
|
||||||
<div class="demo-box">
|
|
||||||
${invoker} ${popup.content}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}); */
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
|
||||||
import { fixtureSync } from '@open-wc/testing-helpers';
|
|
||||||
import { css } from '@lion/core';
|
|
||||||
import { OverlayController } from '../index.js';
|
|
||||||
|
|
||||||
const popupDemoStyle = css`
|
|
||||||
.demo-box {
|
|
||||||
width: 200px;
|
|
||||||
height: 40px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid grey;
|
|
||||||
margin: 240px auto 240px 240px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-popup {
|
|
||||||
display: block;
|
|
||||||
max-width: 250px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.12), 0 6px 6px 0 rgba(0, 0, 0, 0.24);
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
storiesOf('Local Overlay System|Local Overlay', module)
|
|
||||||
.add('Basic', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
hidesOnEsc: true,
|
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @click=${() => popup.toggle()}>UK</button>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box">
|
|
||||||
In the ${popup.invoker}${popup.content} the weather is nice.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('Change preferred position', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
hidesOnEsc: true,
|
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
popperConfig: {
|
|
||||||
placement: 'top-end',
|
|
||||||
},
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @click=${() => popup.toggle()}>UK</button>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box">
|
|
||||||
In the ${popup.invoker}${popup.content} the weather is nice.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('Single placement parameter', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
hidesOnEsc: true,
|
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
popperConfig: {
|
|
||||||
placement: 'bottom',
|
|
||||||
},
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">
|
|
||||||
Supplying placement with a single parameter will assume 'center' for the other.
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @click=${() => popup.toggle()}>UK</button>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box">
|
|
||||||
${popup.invoker}${popup.content}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('On hover', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
hidesOnEsc: true,
|
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
popperConfig: {
|
|
||||||
placement: 'bottom',
|
|
||||||
},
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @mouseenter=${() => popup.show()} @mouseleave=${() => popup.hide()}>UK</button>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box">
|
|
||||||
In the beautiful ${popup.invoker}${popup.content} the weather is nice.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('On an input', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">United Kingdom</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<input
|
|
||||||
id="input"
|
|
||||||
type="text"
|
|
||||||
@focusin=${() => popup.show()}
|
|
||||||
@focusout=${() => popup.hide()}
|
|
||||||
/>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box">
|
|
||||||
<label for="input">Input with a dropdown</label>
|
|
||||||
${popup.invoker}${popup.content}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('trapsKeyboardFocus', () => {
|
|
||||||
const popup = new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
hidesOnEsc: true,
|
|
||||||
hidesOnOutsideClick: true,
|
|
||||||
trapsKeyboardFocus: true,
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-popup">
|
|
||||||
<button id="elem1">Button</button>
|
|
||||||
<a id="elem2" href="#">Anchor</a>
|
|
||||||
<div id="elem3" tabindex="0">Tabindex</div>
|
|
||||||
<input id="elem4" placeholder="Input" />
|
|
||||||
<div id="elem5" contenteditable>Content editable</div>
|
|
||||||
<textarea id="elem6">Textarea</textarea>
|
|
||||||
<select id="elem7">
|
|
||||||
<option>1</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
invokerNode: fixtureSync(html`
|
|
||||||
<button @click=${() => popup.toggle()}>UK</button>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box">
|
|
||||||
${popup.invoker}${popup.content}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
import { storiesOf, html } from '@open-wc/demoing-storybook';
|
|
||||||
import { fixtureSync } from '@open-wc/testing-helpers';
|
|
||||||
import { css } from '@lion/core';
|
|
||||||
import { OverlayController, withModalDialogConfig } from '../index.js';
|
|
||||||
|
|
||||||
const modalDialogDemoStyle = css`
|
|
||||||
.demo-overlay {
|
|
||||||
background-color: white;
|
|
||||||
width: 200px;
|
|
||||||
border: 1px solid lightgrey;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
storiesOf('Global Overlay System|Modal Dialog', module)
|
|
||||||
.add('Default', () => {
|
|
||||||
const nestedDialogCtrl = new OverlayController({
|
|
||||||
...withModalDialogConfig(),
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-overlay" style="margin-top: -100px;">
|
|
||||||
<p>Nested modal dialog</p>
|
|
||||||
<button @click="${() => nestedDialogCtrl.hide()}">Close</button>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const dialogCtrl = new OverlayController({
|
|
||||||
...withModalDialogConfig(),
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-overlay">
|
|
||||||
<p>Modal dialog</p>
|
|
||||||
<button @click="${() => dialogCtrl.hide()}">Close</button>
|
|
||||||
<button
|
|
||||||
@click="${event => nestedDialogCtrl.show(event.target)}"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
Open nested dialog
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${modalDialogDemoStyle}
|
|
||||||
</style>
|
|
||||||
<a href="#">Anchor 1</a>
|
|
||||||
<button
|
|
||||||
@click="${event => dialogCtrl.show(event.target)}"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
Open dialog
|
|
||||||
</button>
|
|
||||||
<a href="#">Anchor 2</a>
|
|
||||||
${Array(50).fill(
|
|
||||||
html`
|
|
||||||
<p>Lorem ipsum</p>
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('Option "isBlocking"', () => {
|
|
||||||
const blockingDialogCtrl = new OverlayController({
|
|
||||||
...withModalDialogConfig(),
|
|
||||||
isBlocking: true,
|
|
||||||
viewportConfig: {
|
|
||||||
placement: 'top',
|
|
||||||
},
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-overlay demo-overlay--2">
|
|
||||||
<p>Hides other dialogs</p>
|
|
||||||
<button @click="${() => blockingDialogCtrl.hide()}">Close</button>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalDialogCtrl = new OverlayController({
|
|
||||||
...withModalDialogConfig(),
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-overlay">
|
|
||||||
<p>Normal dialog</p>
|
|
||||||
<button
|
|
||||||
@click="${event => blockingDialogCtrl.show(event.target)}"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
Open blocking dialog
|
|
||||||
</button>
|
|
||||||
<button @click="${() => normalDialogCtrl.hide()}">Close</button>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${modalDialogDemoStyle}
|
|
||||||
</style>
|
|
||||||
<button
|
|
||||||
@click="${event => normalDialogCtrl.show(event.target)}"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
Open dialog
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
@ -11,39 +11,8 @@ const globalOverlayDemoStyle = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
storiesOf('Global Overlay System|Global Overlay', module)
|
storiesOf('Overlay System | Behavior Features', module)
|
||||||
.add('Default', () => {
|
.add('preventsScroll', () => {
|
||||||
const overlayCtrl = new OverlayController({
|
|
||||||
placementMode: 'global',
|
|
||||||
contentNode: fixtureSync(html`
|
|
||||||
<div class="demo-overlay">
|
|
||||||
<p>Simple overlay</p>
|
|
||||||
<button @click="${() => overlayCtrl.hide()}">Close</button>
|
|
||||||
</div>
|
|
||||||
`),
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
${globalOverlayDemoStyle}
|
|
||||||
</style>
|
|
||||||
<a href="#">Anchor 1</a>
|
|
||||||
<button
|
|
||||||
@click="${event => overlayCtrl.show(event.target)}"
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
Open overlay
|
|
||||||
</button>
|
|
||||||
<a href="#">Anchor 2</a>
|
|
||||||
${Array(50).fill(
|
|
||||||
html`
|
|
||||||
<p>Lorem ipsum</p>
|
|
||||||
`,
|
|
||||||
)}
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.add('Option "preventsScroll"', () => {
|
|
||||||
const overlayCtrl = new OverlayController({
|
const overlayCtrl = new OverlayController({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
preventsScroll: true,
|
preventsScroll: true,
|
||||||
|
|
@ -73,7 +42,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
)}
|
)}
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.add('Option "hasBackdrop"', () => {
|
.add('hasBackdrop', () => {
|
||||||
const overlayCtrl = new OverlayController({
|
const overlayCtrl = new OverlayController({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
hasBackdrop: true,
|
hasBackdrop: true,
|
||||||
|
|
@ -98,7 +67,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.add('Option "trapsKeyboardFocus"', () => {
|
.add('trapsKeyboardFocus', () => {
|
||||||
const overlayCtrl = new OverlayController({
|
const overlayCtrl = new OverlayController({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
trapsKeyboardFocus: true,
|
trapsKeyboardFocus: true,
|
||||||
|
|
@ -135,7 +104,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
<a href="#">Anchor 2</a>
|
<a href="#">Anchor 2</a>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.add('Option "trapsKeyboardFocus" (multiple)', () => {
|
.add('trapsKeyboardFocus" (multiple)', () => {
|
||||||
const overlayCtrl2 = new OverlayController({
|
const overlayCtrl2 = new OverlayController({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
trapsKeyboardFocus: true,
|
trapsKeyboardFocus: true,
|
||||||
|
|
@ -183,7 +152,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
<a href="#">Anchor 2</a>
|
<a href="#">Anchor 2</a>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.add('Option "isBlocking"', () => {
|
.add('isBlocking', () => {
|
||||||
const blockingOverlayCtrl = new OverlayController({
|
const blockingOverlayCtrl = new OverlayController({
|
||||||
placementMode: 'global',
|
placementMode: 'global',
|
||||||
isBlocking: true,
|
isBlocking: true,
|
||||||
|
|
@ -228,7 +197,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.add('Option "viewportConfig:placement"', () => {
|
.add('viewportConfig:placement', () => {
|
||||||
const tagName = 'lion-overlay-placement-demo';
|
const tagName = 'lion-overlay-placement-demo';
|
||||||
if (!customElements.get(tagName)) {
|
if (!customElements.get(tagName)) {
|
||||||
customElements.define(
|
customElements.define(
|
||||||
|
|
@ -241,18 +210,9 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
constructor() {
|
||||||
return html`
|
super();
|
||||||
<p>Overlay placement: ${this.placement}</p>
|
this.options = [
|
||||||
<button @click="${this._togglePlacement}">
|
|
||||||
Toggle ${this.placement} position
|
|
||||||
</button>
|
|
||||||
<button @click="${() => this.dispatchEvent(new CustomEvent('close'))}">Close</button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_togglePlacement() {
|
|
||||||
const options = [
|
|
||||||
'top',
|
'top',
|
||||||
'top-right',
|
'top-right',
|
||||||
'right',
|
'right',
|
||||||
|
|
@ -263,7 +223,24 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
'top-left',
|
'top-left',
|
||||||
'center',
|
'center',
|
||||||
];
|
];
|
||||||
this.placement = options[(options.indexOf(this.placement) + 1) % options.length];
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<p>Overlay placement: ${this.placement}</p>
|
||||||
|
<button @click="${this._togglePlacement}">
|
||||||
|
Toggle
|
||||||
|
${this.options[(this.options.indexOf(this.placement) + 1) % this.options.length]}
|
||||||
|
position
|
||||||
|
</button>
|
||||||
|
<button @click="${() => this.dispatchEvent(new CustomEvent('close'))}">Close</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_togglePlacement() {
|
||||||
|
this.placement = this.options[
|
||||||
|
(this.options.indexOf(this.placement) + 1) % this.options.length
|
||||||
|
];
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('toggle-placement', {
|
new CustomEvent('toggle-placement', {
|
||||||
detail: this.placement,
|
detail: this.placement,
|
||||||
|
|
@ -300,7 +277,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.add('Option "hidesOnOutsideClick"', () => {
|
.add('hidesOnOutsideClick', () => {
|
||||||
const shadowContent = document.createElement('div');
|
const shadowContent = document.createElement('div');
|
||||||
shadowContent.attachShadow({ mode: 'open' });
|
shadowContent.attachShadow({ mode: 'open' });
|
||||||
shadowContent.shadowRoot.appendChild(
|
shadowContent.shadowRoot.appendChild(
|
||||||
|
|
@ -860,7 +860,7 @@ describe('OverlayController', () => {
|
||||||
expect(ctrl.contentNode.textContent).to.include('content2');
|
expect(ctrl.contentNode.textContent).to.include('content2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects the inital config provided to new OverlayController(initialConfig)', async () => {
|
it('respects the initial config provided to new OverlayController(initialConfig)', async () => {
|
||||||
const contentNode = fixtureSync(html`
|
const contentNode = fixtureSync(html`
|
||||||
<div>my content</div>
|
<div>my content</div>
|
||||||
`);
|
`);
|
||||||
|
|
@ -880,6 +880,33 @@ describe('OverlayController', () => {
|
||||||
expect(ctrl.handlesAccesibility).to.equal(true);
|
expect(ctrl.handlesAccesibility).to.equal(true);
|
||||||
expect(ctrl.contentNode).to.equal(contentNode);
|
expect(ctrl.contentNode).to.equal(contentNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('allows for updating viewport config placement only, while keeping the content shown', async () => {
|
||||||
|
const contentNode = fixtureSync(html`
|
||||||
|
<div>my content</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
// This is the shared config
|
||||||
|
placementMode: 'global',
|
||||||
|
handlesAccesibility: true,
|
||||||
|
contentNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctrl.show();
|
||||||
|
expect(
|
||||||
|
ctrl._contentNodeWrapper.classList.contains('global-overlays__overlay-container--center'),
|
||||||
|
);
|
||||||
|
expect(ctrl.isShown).to.be.true;
|
||||||
|
|
||||||
|
ctrl.updateConfig({ viewportConfig: { placement: 'top-right' } });
|
||||||
|
expect(
|
||||||
|
ctrl._contentNodeWrapper.classList.contains(
|
||||||
|
'global-overlays__overlay-container--top-right',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(ctrl.isShown).to.be.true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
describe('Accessibility', () => {
|
||||||
|
|
|
||||||
79
packages/overlays/test/lion-overlay.test.js
Normal file
79
packages/overlays/test/lion-overlay.test.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import '../lion-overlay.js';
|
||||||
|
|
||||||
|
describe('lion-overlay', () => {
|
||||||
|
describe('Basic', () => {
|
||||||
|
it('should not be shown by default', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-overlay>
|
||||||
|
<div slot="content">Hey there</div>
|
||||||
|
<lion-button slot="invoker">Invoker button</lion-button>
|
||||||
|
</lion-overlay>
|
||||||
|
`);
|
||||||
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show content on invoker click', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-overlay>
|
||||||
|
<div slot="content">
|
||||||
|
Hey there
|
||||||
|
</div>
|
||||||
|
<lion-button slot="invoker">Invoker button</lion-button>
|
||||||
|
</lion-overlay>
|
||||||
|
`);
|
||||||
|
const invoker = el.querySelector('[slot="invoker"]');
|
||||||
|
invoker.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el._overlayCtrl.isShown).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide content on close event', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-overlay>
|
||||||
|
<div slot="content">
|
||||||
|
Hey there
|
||||||
|
<button @click=${e => e.target.dispatchEvent(new Event('close', { bubbles: true }))}>
|
||||||
|
x
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<lion-button slot="invoker">Invoker button</lion-button>
|
||||||
|
</lion-overlay>
|
||||||
|
`);
|
||||||
|
const invoker = el.querySelector('[slot="invoker"]');
|
||||||
|
invoker.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el._overlayCtrl.isShown).to.be.true;
|
||||||
|
|
||||||
|
const closeBtn = el._overlayCtrl.contentNode.querySelector('button');
|
||||||
|
closeBtn.click();
|
||||||
|
await el.updateComplete;
|
||||||
|
|
||||||
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond to initially and dynamically setting the config', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-overlay
|
||||||
|
.config=${{ trapsKeyboardFocus: false, viewportConfig: { placement: 'top' } }}
|
||||||
|
>
|
||||||
|
<div slot="content">Hey there</div>
|
||||||
|
<lion-button slot="invoker">Invoker button</lion-button>
|
||||||
|
</lion-overlay>
|
||||||
|
`);
|
||||||
|
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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,42 +1,16 @@
|
||||||
import { LitElement, html } from '@lion/core';
|
import { OverlayController, LionOverlay } from '@lion/overlays';
|
||||||
import { OverlayMixin, OverlayController } from '@lion/overlays';
|
|
||||||
|
|
||||||
export class LionPopup extends OverlayMixin(LitElement) {
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<slot name="invoker"></slot>
|
|
||||||
<slot name="content"></slot>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: This should be refactored to Array.from(this.children).find(child => child.slot === 'content')
|
|
||||||
// When this issue is fixed https://github.com/ing-bank/lion/issues/382
|
|
||||||
get _overlayContentNode() {
|
|
||||||
return this.querySelector('[slot="content"]');
|
|
||||||
}
|
|
||||||
|
|
||||||
get _overlayInvokerNode() {
|
|
||||||
return Array.from(this.children).find(child => child.slot === 'invoker');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export class LionPopup extends LionOverlay {
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_defineOverlay() {
|
_defineOverlay() {
|
||||||
return new OverlayController({
|
return new OverlayController({
|
||||||
placementMode: 'local',
|
placementMode: 'local',
|
||||||
|
hidesOnOutsideClick: true,
|
||||||
|
hidesOnEsc: true,
|
||||||
contentNode: this._overlayContentNode,
|
contentNode: this._overlayContentNode,
|
||||||
invokerNode: this._overlayInvokerNode,
|
invokerNode: this._overlayInvokerNode,
|
||||||
handlesAccessibility: true,
|
handlesAccessibility: true,
|
||||||
|
...this.config,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.__toggle = () => this._overlayCtrl.toggle();
|
|
||||||
this._overlayInvokerNode.addEventListener('click', this.__toggle);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._overlayInvokerNode.removeEventListener('click', this._toggle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ const popupDemoStyle = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
storiesOf('Local Overlay System|Popup', module)
|
storiesOf('Overlays Specific WC|Popup', module)
|
||||||
.addDecorator(withKnobs)
|
.addDecorator(withKnobs)
|
||||||
.add(
|
.add(
|
||||||
'Button popup',
|
'Button popup',
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,8 @@ export class LionSelectRich extends OverlayMixin(
|
||||||
this.modelValue.length > 0
|
this.modelValue.length > 0
|
||||||
) {
|
) {
|
||||||
if (this.checkedIndex) {
|
if (this.checkedIndex) {
|
||||||
|
// Necessary to sync the checkedIndex through the getter/setter explicitly
|
||||||
|
// eslint-disable-next-line no-self-assign
|
||||||
this.checkedIndex = this.checkedIndex;
|
this.checkedIndex = this.checkedIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,15 @@ export class LionTooltip extends LionPopup {
|
||||||
super();
|
super();
|
||||||
this.mouseActive = false;
|
this.mouseActive = false;
|
||||||
this.keyActive = false;
|
this.keyActive = false;
|
||||||
|
|
||||||
|
// Trigger config setter to ensure it updates in OverlayController
|
||||||
|
this.config = {
|
||||||
|
...this.config,
|
||||||
|
elementToFocusAfterHide: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
_setupShowHideListeners() {
|
||||||
super.connectedCallback();
|
|
||||||
this._overlayContentNode.setAttribute('role', 'tooltip');
|
|
||||||
|
|
||||||
this.__resetActive = () => {
|
this.__resetActive = () => {
|
||||||
this.mouseActive = false;
|
this.mouseActive = false;
|
||||||
this.keyActive = false;
|
this.keyActive = false;
|
||||||
|
|
@ -19,26 +22,26 @@ export class LionTooltip extends LionPopup {
|
||||||
this.__showMouse = () => {
|
this.__showMouse = () => {
|
||||||
if (!this.keyActive) {
|
if (!this.keyActive) {
|
||||||
this.mouseActive = true;
|
this.mouseActive = true;
|
||||||
this._overlayCtrl.show();
|
this.opened = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.__hideMouse = () => {
|
this.__hideMouse = () => {
|
||||||
if (!this.keyActive) {
|
if (!this.keyActive) {
|
||||||
this._overlayCtrl.hide();
|
this.opened = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.__showKey = () => {
|
this.__showKey = () => {
|
||||||
if (!this.mouseActive) {
|
if (!this.mouseActive) {
|
||||||
this.keyActive = true;
|
this.keyActive = true;
|
||||||
this._overlayCtrl.show();
|
this.opened = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.__hideKey = () => {
|
this.__hideKey = () => {
|
||||||
if (!this.mouseActive) {
|
if (!this.mouseActive) {
|
||||||
this._overlayCtrl.hide();
|
this.opened = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -49,12 +52,16 @@ export class LionTooltip extends LionPopup {
|
||||||
this._overlayInvokerNode.addEventListener('focusout', this.__hideKey);
|
this._overlayInvokerNode.addEventListener('focusout', this.__hideKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
_teardownShowHideListeners() {
|
||||||
super.disconnectedCallback();
|
|
||||||
this._overlayCtrl.removeEventListener('hide', this.__resetActive);
|
this._overlayCtrl.removeEventListener('hide', this.__resetActive);
|
||||||
this.removeEventListener('mouseenter', this.__showMouse);
|
this.removeEventListener('mouseenter', this.__showMouse);
|
||||||
this.removeEventListener('mouseleave', this._hideMouse);
|
this.removeEventListener('mouseleave', this._hideMouse);
|
||||||
this._overlayInvokerNode.removeEventListener('focusin', this._showKey);
|
this._overlayInvokerNode.removeEventListener('focusin', this._showKey);
|
||||||
this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
|
this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._overlayContentNode.setAttribute('role', 'tooltip');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ const tooltipDemoStyle = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
storiesOf('Local Overlay System|Tooltip', module)
|
storiesOf('Overlays Specific WC|Tooltip', module)
|
||||||
.addDecorator(withKnobs)
|
.addDecorator(withKnobs)
|
||||||
.add(
|
.add(
|
||||||
'Button tooltip',
|
'Button tooltip',
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Use the `.add` method to add async functions to the queue
|
||||||
|
* Await the `.complete` if you want to ensure the queue is empty at any point
|
||||||
|
* `complete` resolves whenever no more tasks are running.
|
||||||
|
* Important note: Currently runs tasks 1 by 1, there is no concurrency option at the moment
|
||||||
|
*/
|
||||||
export class AsyncQueue {
|
export class AsyncQueue {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.__running = false;
|
this.__running = false;
|
||||||
|
|
@ -7,7 +13,7 @@ export class AsyncQueue {
|
||||||
add(task) {
|
add(task) {
|
||||||
this.__queue.push(task);
|
this.__queue.push(task);
|
||||||
if (!this.__running) {
|
if (!this.__running) {
|
||||||
// aka we have a new queue, because before there was nothing in the queue
|
// We have a new queue, because before there was nothing in the queue
|
||||||
this.complete = new Promise(resolve => {
|
this.complete = new Promise(resolve => {
|
||||||
this.__callComplete = resolve;
|
this.__callComplete = resolve;
|
||||||
});
|
});
|
||||||
|
|
@ -22,7 +28,6 @@ export class AsyncQueue {
|
||||||
if (this.__queue.length > 0) {
|
if (this.__queue.length > 0) {
|
||||||
this.__run();
|
this.__run();
|
||||||
} else {
|
} else {
|
||||||
// queue is empty again, so call complete
|
|
||||||
this.__running = false;
|
this.__running = false;
|
||||||
this.__callComplete();
|
this.__callComplete();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,13 @@ import '../packages/icon/stories/index.stories.js';
|
||||||
import '../packages/ajax/stories/index.stories.js';
|
import '../packages/ajax/stories/index.stories.js';
|
||||||
import '../packages/steps/stories/index.stories.js';
|
import '../packages/steps/stories/index.stories.js';
|
||||||
import '../packages/localize/stories/index.stories.js';
|
import '../packages/localize/stories/index.stories.js';
|
||||||
|
import '../packages/calendar/stories/index.stories.js';
|
||||||
|
|
||||||
import '../packages/overlays/stories/index.stories.js';
|
import '../packages/overlays/stories/index.stories.js';
|
||||||
|
import '../packages/overlays/stories/overlay-features.stories.js';
|
||||||
|
import '../packages/dialog/stories/index.stories.js';
|
||||||
import '../packages/popup/stories/index.stories.js';
|
import '../packages/popup/stories/index.stories.js';
|
||||||
import '../packages/tooltip/stories/index.stories.js';
|
import '../packages/tooltip/stories/index.stories.js';
|
||||||
import '../packages/calendar/stories/index.stories.js';
|
|
||||||
|
|
||||||
import '../packages/select-rich/stories/index.stories.js';
|
import '../packages/select-rich/stories/index.stories.js';
|
||||||
import '../packages/switch/stories/index.stories.js';
|
import '../packages/switch/stories/index.stories.js';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue