feat: refactor the overlay system implementations, docs and demos

This commit is contained in:
Joren Broekema 2019-11-26 11:18:51 +01:00 committed by Thomas Allmer
parent 29cf5ac188
commit a5a9f975a6
40 changed files with 2482 additions and 2011 deletions

45
packages/dialog/README.md Normal file
View 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
View file

@ -0,0 +1 @@
export { LionDialog } from './src/LionDialog.js';

View file

@ -0,0 +1,3 @@
import { LionDialog } from './src/LionDialog.js';
customElements.define('lion-dialog', LionDialog);

View 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"
}
}

View 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
});
}
}

View 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>
`;
});

View 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',
),
);
});
});
});

View file

@ -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()}>

View file

@ -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() {

View file

@ -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 dont expect a dialog to open (at this moment in time) but expect a sub-menu. Until support for the dialog value has better implementation, its 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 dont expect a dialog to open (at this moment in time) but expect a sub-menu. Until support for the dialog value has better implementation, its probably best to not use aria-haspopup on the element that opens the modal dialog.

View file

@ -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';
```

View file

@ -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.

View file

@ -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.

View 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.

View file

@ -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'.

View file

@ -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:
- Its 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) cant be apparent. Additionally, making a modal dialog requiring values or transforms) cant be apparent. Additionally, making a modal dialog requiring

View file

@ -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.

View 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>
`;
```

View file

@ -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';

View file

@ -0,0 +1,3 @@
import { LionOverlay } from './src/LionOverlay.js';
customElements.define('lion-overlay', LionOverlay);

View file

@ -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",

View 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();
}
}

View file

@ -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' });
} }

View file

@ -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>
`,
)}
`;
});

View file

@ -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>
`;
},
);

View file

@ -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>
`;
}) */

View file

@ -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>
`;
}); */

View file

@ -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>
`;
});

View file

@ -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>
`;
});

View file

@ -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(

View file

@ -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', () => {

View 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',
),
);
});
});
});

View file

@ -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);
}
} }

View file

@ -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',

View file

@ -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;
} }
} }

View file

@ -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');
}
} }

View file

@ -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',

View file

@ -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();
} }

View file

@ -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';

1968
yarn.lock

File diff suppressed because it is too large Load diff