Merge pull request #388 from ing-bank/feat/dialog
feat: add lion-dialog as primary element for overlays
This commit is contained in:
commit
71d2d2762d
51 changed files with 2714 additions and 2508 deletions
10
README.md
10
README.md
|
|
@ -40,7 +40,7 @@ The accessibility column indicates whether the functionality is accessible in it
|
||||||
| [icon](./packages/icon) | [](https://www.npmjs.com/package/@lion/icon) | Display our svg icons | [#173][i173], [#172][i172] |
|
| [icon](./packages/icon) | [](https://www.npmjs.com/package/@lion/icon) | Display our svg icons | [#173][i173], [#172][i172] |
|
||||||
| [steps](./packages/steps) | [](https://www.npmjs.com/package/@lion/steps) | Multi Step System | n/a |
|
| [steps](./packages/steps) | [](https://www.npmjs.com/package/@lion/steps) | Multi Step System | n/a |
|
||||||
| [tabs](./packages/tabs) | [](https://www.npmjs.com/package/@lion/tabs) | Move between a small number of equally important views | n/a |
|
| [tabs](./packages/tabs) | [](https://www.npmjs.com/package/@lion/tabs) | Move between a small number of equally important views | n/a |
|
||||||
| **-- Forms --** | | |
|
| **-- Forms --** | | | |
|
||||||
| [form](./packages/form) | [](https://www.npmjs.com/package/@lion/form) | Wrapper for multiple form elements | ✔️ |
|
| [form](./packages/form) | [](https://www.npmjs.com/package/@lion/form) | Wrapper for multiple form elements | ✔️ |
|
||||||
| [field](./packages/field) | [](https://www.npmjs.com/package/@lion/field) | Base Class for all inputs | [#190][i190] |
|
| [field](./packages/field) | [](https://www.npmjs.com/package/@lion/field) | Base Class for all inputs | [#190][i190] |
|
||||||
| [fieldset](./packages/fieldset) | [](https://www.npmjs.com/package/@lion/fieldset) | Group for form inputs | ✔️ |
|
| [fieldset](./packages/fieldset) | [](https://www.npmjs.com/package/@lion/fieldset) | Group for form inputs | ✔️ |
|
||||||
|
|
@ -48,7 +48,7 @@ The accessibility column indicates whether the functionality is accessible in it
|
||||||
| [checkbox](./packages/checkbox) | [](https://www.npmjs.com/package/@lion/checkbox) | Checkbox form element | ✔️ |
|
| [checkbox](./packages/checkbox) | [](https://www.npmjs.com/package/@lion/checkbox) | Checkbox form element | ✔️ |
|
||||||
| [checkbox-group](./packages/checkbox-group) | [](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes | ✔️ |
|
| [checkbox-group](./packages/checkbox-group) | [](https://www.npmjs.com/package/@lion/checkbox-group) | Group of checkboxes | ✔️ |
|
||||||
| [input](./packages/input) | [](https://www.npmjs.com/package/@lion/input) | Input element for strings | ✔️ |
|
| [input](./packages/input) | [](https://www.npmjs.com/package/@lion/input) | Input element for strings | ✔️ |
|
||||||
| [input-amount](./packages/input-amount) | [](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | [#166][i166] | ✔️ |
|
| [input-amount](./packages/input-amount) | [](https://www.npmjs.com/package/@lion/input-amount) | Input element for amounts | [#166][i166] |
|
||||||
| [input-date](./packages/input-date) | [](https://www.npmjs.com/package/@lion/input-date) | Input element for dates | ✔️ |
|
| [input-date](./packages/input-date) | [](https://www.npmjs.com/package/@lion/input-date) | Input element for dates | ✔️ |
|
||||||
| [input-datepicker](./packages/input-datepicker) | [](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
|
| [input-datepicker](./packages/input-datepicker) | [](https://www.npmjs.com/package/@lion/input-datepicker) | Input element for dates with a datepicker | ✔️ |
|
||||||
| [input-email](./packages/input-email) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | [#169][i169] |
|
| [input-email](./packages/input-email) | [](https://www.npmjs.com/package/@lion/input-email) | Input element for e-mails | [#169][i169] |
|
||||||
|
|
@ -58,9 +58,9 @@ The accessibility column indicates whether the functionality is accessible in it
|
||||||
| [select](./packages/select) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
|
| [select](./packages/select) | [](https://www.npmjs.com/package/@lion/select) | Simple native dropdown element | ✔️ |
|
||||||
| [textarea](./packages/textarea) | [](https://www.npmjs.com/package/@lion/textarea) | Multiline text input | [#165][i165] |
|
| [textarea](./packages/textarea) | [](https://www.npmjs.com/package/@lion/textarea) | Multiline text input | [#165][i165] |
|
||||||
| **-- Overlays --** | | | |
|
| **-- Overlays --** | | | |
|
||||||
| [overlays](./packages/overlays) | [](https://www.npmjs.com/package/@lion/overlays) | Overlays System using lit-html for rendering | ✔️ |
|
| [overlays](./packages/overlays) | [](https://www.npmjs.com/package/@lion/overlays) | Overlay System | ✔️ |
|
||||||
| [popup](./packages/popup) | [](https://www.npmjs.com/package/@lion/popup) | Popup element | [#175][i175], [#174][i174] |
|
| [dialog](./packages/dialog) | [](https://www.npmjs.com/package/@lion/dialog) | Dialog element | ✔️ |
|
||||||
| [tooltip](./packages/tooltip) | [](https://www.npmjs.com/package/@lion/tooltip) | Popup element | [#178][i178], [#177][i177], [#176][i176], [#175][i175], [#174][i174] |
|
| [tooltip](./packages/tooltip) | [](https://www.npmjs.com/package/@lion/tooltip) | Tooltip element | [#178][i178], [#177][i177], [#176][i176], [#175][i175], [#174][i174] |
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,10 +108,16 @@ describe('<lion-calendar>', () => {
|
||||||
`),
|
`),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
elObj.checkForAllDayObjs(o => o.buttonEl.getAttribute('tabindex') === '0', n => n === 5),
|
elObj.checkForAllDayObjs(
|
||||||
|
o => o.buttonEl.getAttribute('tabindex') === '0',
|
||||||
|
n => n === 5,
|
||||||
|
),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
expect(
|
expect(
|
||||||
elObj.checkForAllDayObjs(o => o.buttonEl.getAttribute('tabindex') === '-1', n => n !== 5),
|
elObj.checkForAllDayObjs(
|
||||||
|
o => o.buttonEl.getAttribute('tabindex') === '-1',
|
||||||
|
n => n !== 5,
|
||||||
|
),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
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.config=${{
|
||||||
|
viewportConfig: { placement: 'bottom-right' },
|
||||||
|
}}>
|
||||||
|
<div slot="content">
|
||||||
|
This is a dialog
|
||||||
|
<button
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('dialog-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);
|
||||||
40
packages/dialog/package.json
Normal file
40
packages/dialog/package.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"@open-wc/demoing-storybook": "^0.2.0",
|
||||||
|
"@open-wc/testing": "^2.3.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/dialog/src/LionDialog.js
Normal file
39
packages/dialog/src/LionDialog.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { withModalDialogConfig, OverlayMixin } from '@lion/overlays';
|
||||||
|
import { LitElement, html } from '@lion/core';
|
||||||
|
|
||||||
|
export class LionDialog extends OverlayMixin(LitElement) {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.closeEventName = 'dialog-close';
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return {
|
||||||
|
...withModalDialogConfig(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
|
this.__toggle = () => {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.addEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOpenCloseListeners() {
|
||||||
|
if (this._overlayInvokerNode) {
|
||||||
|
this._overlayInvokerNode.removeEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
142
packages/dialog/stories/index.stories.js
Normal file
142
packages/dialog/stories/index.stories.js
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { storiesOf, html, withKnobs, object } from '@open-wc/demoing-storybook';
|
||||||
|
import { css } from '@lion/core';
|
||||||
|
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>hide</code> event.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For the dialog to close, it will need to bubble to the content slot (use
|
||||||
|
<code>bubbles: true</code>. If absolutely needed <code>composed: true</code> can be used to
|
||||||
|
traverse shadow boundaries)
|
||||||
|
</p>
|
||||||
|
<p>The demo below demonstrates this</p>
|
||||||
|
<div class="demo-box">
|
||||||
|
<lion-dialog>
|
||||||
|
<button slot="invoker">Dialog</button>
|
||||||
|
<div slot="content" class="dialog">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Custom configuration', () => {
|
||||||
|
const dialog = placement => html`
|
||||||
|
<lion-dialog .config=${{ viewportConfig: { placement } }}>
|
||||||
|
<button slot="invoker">Dialog ${placement}</button>
|
||||||
|
<div slot="content" class="dialog">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</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' } })}>
|
||||||
|
<button slot="invoker">Dialog</button>
|
||||||
|
<div slot="content" class="dialog">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-dialog>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${dialogDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
${dialog}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
34
packages/dialog/test/lion-dialog.test.js
Normal file
34
packages/dialog/test/lion-dialog.test.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { runOverlayMixinSuite } from '@lion/overlays/test-suites/OverlayMixin.suite.js';
|
||||||
|
|
||||||
|
import '../lion-dialog.js';
|
||||||
|
|
||||||
|
describe('lion-dialog', () => {
|
||||||
|
describe('Integration tests', () => {
|
||||||
|
const tagString = 'lion-dialog';
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
||||||
|
runOverlayMixinSuite({
|
||||||
|
tagString,
|
||||||
|
tag,
|
||||||
|
suffix: ' for lion-dialog',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Basic', () => {
|
||||||
|
it('should show content on invoker click', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<lion-dialog>
|
||||||
|
<div slot="content" class="dialog">
|
||||||
|
Hey there
|
||||||
|
</div>
|
||||||
|
<button slot="invoker">Popup button</button>
|
||||||
|
</lion-dialog>
|
||||||
|
`);
|
||||||
|
const invoker = el.querySelector('[slot="invoker"]');
|
||||||
|
invoker.click();
|
||||||
|
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -78,7 +78,6 @@ describe('FormControlMixin', () => {
|
||||||
const el = wrapper.querySelector(elem);
|
const el = wrapper.querySelector(elem);
|
||||||
|
|
||||||
const { _inputNode } = el;
|
const { _inputNode } = el;
|
||||||
console.log('_inputNode', _inputNode);
|
|
||||||
|
|
||||||
// 1. addToAriaLabelledBy()
|
// 1. addToAriaLabelledBy()
|
||||||
// external inputs should go in order defined by user
|
// external inputs should go in order defined by user
|
||||||
|
|
|
||||||
|
|
@ -177,16 +177,28 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(el.modelValue).to.deep.equal({
|
expect(el.modelValue).to.deep.equal({
|
||||||
lastName: 'Bar',
|
lastName: 'Bar',
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'football' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: true, value: 'chess' },
|
||||||
|
{ checked: false, value: 'football' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: false, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
color: { checked: false, value: 'blue' },
|
color: { checked: false, value: 'blue' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
el.modelValue = {
|
el.modelValue = {
|
||||||
lastName: 2,
|
lastName: 2,
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [{ checked: true, value: 'chess' }, { checked: false, value: 'baseball' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: true, value: 'chess' },
|
||||||
|
{ checked: false, value: 'baseball' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: false, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
color: { checked: false, value: 'blue' },
|
color: { checked: false, value: 'blue' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -546,7 +558,10 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(fieldset.formElements['hobbies[]'][0].serializedValue).to.equal('Bar-serialized');
|
expect(fieldset.formElements['hobbies[]'][0].serializedValue).to.equal('Bar-serialized');
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||||
'hobbies[]': ['Bar-serialized', { checked: false, value: 'rugby' }],
|
'hobbies[]': ['Bar-serialized', { checked: false, value: 'rugby' }],
|
||||||
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
|
'gender[]': [
|
||||||
|
{ checked: false, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
color: { checked: false, value: 'blue' },
|
color: { checked: false, value: 'blue' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -562,15 +577,27 @@ describe('<lion-fieldset>', () => {
|
||||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||||
|
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||||
'hobbies[]': [{ checked: true, value: 'football' }, { checked: false, value: 'rugby' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: true, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: true, value: 'football' },
|
||||||
|
{ checked: false, value: 'rugby' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: true, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
color: { checked: false, value: 'blue' },
|
color: { checked: false, value: 'blue' },
|
||||||
});
|
});
|
||||||
fieldset.formElements.color.disabled = true;
|
fieldset.formElements.color.disabled = true;
|
||||||
|
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||||
'hobbies[]': [{ checked: true, value: 'football' }, { checked: false, value: 'rugby' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: true, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: true, value: 'football' },
|
||||||
|
{ checked: false, value: 'rugby' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: true, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -594,8 +621,14 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||||
comment: 'Foo',
|
comment: 'Foo',
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: false, value: 'chess' },
|
||||||
|
{ checked: false, value: 'rugby' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: false, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
color: { checked: false, value: 'blue' },
|
color: { checked: false, value: 'blue' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -622,8 +655,14 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||||
comment: 'Foo',
|
comment: 'Foo',
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: false, value: 'chess' },
|
||||||
|
{ checked: false, value: 'rugby' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: false, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -631,8 +670,14 @@ describe('<lion-fieldset>', () => {
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||||
comment: 'Foo',
|
comment: 'Foo',
|
||||||
newfieldset: {
|
newfieldset: {
|
||||||
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: false, value: 'chess' },
|
||||||
|
{ checked: false, value: 'rugby' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: false, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
color: { checked: false, value: 'blue' },
|
color: { checked: false, value: 'blue' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -647,8 +692,14 @@ describe('<lion-fieldset>', () => {
|
||||||
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
fieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||||
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
fieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||||
expect(fieldset.serializeGroup()).to.deep.equal({
|
expect(fieldset.serializeGroup()).to.deep.equal({
|
||||||
'hobbies[]': [{ checked: false, value: 'chess' }, { checked: false, value: 'rugby' }],
|
'hobbies[]': [
|
||||||
'gender[]': [{ checked: false, value: 'male' }, { checked: false, value: 'female' }],
|
{ checked: false, value: 'chess' },
|
||||||
|
{ checked: false, value: 'rugby' },
|
||||||
|
],
|
||||||
|
'gender[]': [
|
||||||
|
{ checked: false, value: 'male' },
|
||||||
|
{ checked: false, value: 'female' },
|
||||||
|
],
|
||||||
color: { checked: false, value: 'blue' },
|
color: { checked: false, value: 'blue' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -90,14 +90,9 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
__dispatchHideEvent() {
|
||||||
super();
|
|
||||||
this.__dispatchCloseEvent = this.__dispatchCloseEvent.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
__dispatchCloseEvent() {
|
|
||||||
// Designed to work in conjunction with ModalDialogController
|
// Designed to work in conjunction with ModalDialogController
|
||||||
this.dispatchEvent(new CustomEvent('dialog-close'), { bubbles: true, composed: true });
|
this.dispatchEvent(new CustomEvent('hide'), { bubbles: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -109,7 +104,7 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
|
||||||
<slot name="heading"></slot>
|
<slot name="heading"></slot>
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
@click="${this.__dispatchCloseEvent}"
|
@click="${this.__dispatchHideEvent}"
|
||||||
id="close-button"
|
id="close-button"
|
||||||
title="${this.msgLit('lion-calendar-overlay-frame:close')}"
|
title="${this.msgLit('lion-calendar-overlay-frame:close')}"
|
||||||
aria-label="${this.msgLit('lion-calendar-overlay-frame:close')}"
|
aria-label="${this.msgLit('lion-calendar-overlay-frame:close')}"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { html, ifDefined, render } from '@lion/core';
|
import { html, ifDefined, render } from '@lion/core';
|
||||||
import { LionInputDate } from '@lion/input-date';
|
import { LionInputDate } from '@lion/input-date';
|
||||||
import { OverlayController, withModalDialogConfig, OverlayMixin } from '@lion/overlays';
|
import { withModalDialogConfig, OverlayMixin } from '@lion/overlays';
|
||||||
|
|
||||||
import '@lion/calendar/lion-calendar.js';
|
import '@lion/calendar/lion-calendar.js';
|
||||||
import './lion-calendar-overlay-frame.js';
|
import './lion-calendar-overlay-frame.js';
|
||||||
|
|
@ -205,14 +205,22 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
||||||
* Important: do not change the name of this method.
|
* Important: do not change the name of this method.
|
||||||
*/
|
*/
|
||||||
_overlayTemplate() {
|
_overlayTemplate() {
|
||||||
|
// TODO: add performance optimization to only render the calendar if needed
|
||||||
return html`
|
return html`
|
||||||
<lion-calendar-overlay-frame @dialog-close=${() => this._overlayCtrl.hide()}>
|
<lion-calendar-overlay-frame>
|
||||||
<span slot="heading">${this.calendarHeading}</span>
|
<span slot="heading">${this.calendarHeading}</span>
|
||||||
${this._calendarTemplate()}
|
${this._calendarTemplate()}
|
||||||
</lion-calendar-overlay-frame>
|
</lion-calendar-overlay-frame>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
${this.labelTemplate()} ${this.helpTextTemplate()} ${this.inputGroupTemplate()}
|
||||||
|
${this.feedbackTemplate()} ${this._overlayTemplate()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subclassers can replace this with their custom extension of
|
* Subclassers can replace this with their custom extension of
|
||||||
* LionCalendar, like `<my-calendar id="calendar"></my-calendar>`
|
* LionCalendar, like `<my-calendar id="calendar"></my-calendar>`
|
||||||
|
|
@ -254,19 +262,15 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override Configures OverlayMixin
|
* @override Configures OverlayMixin
|
||||||
* @desc returns an instance of a (dynamic) overlay controller
|
* @desc overrides default configuration options for this component
|
||||||
* @returns {OverlayController}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_defineOverlay({ contentNode, invokerNode }) {
|
_defineOverlayConfig() {
|
||||||
const ctrl = new OverlayController({
|
return {
|
||||||
...withModalDialogConfig(),
|
...withModalDialogConfig(),
|
||||||
contentNode,
|
|
||||||
invokerNode,
|
|
||||||
elementToFocusAfterHide: invokerNode,
|
|
||||||
hidesOnOutsideClick: true,
|
hidesOnOutsideClick: true,
|
||||||
});
|
};
|
||||||
return ctrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async __openCalendarOverlay() {
|
async __openCalendarOverlay() {
|
||||||
|
|
@ -334,4 +338,11 @@ export class LionInputDatepicker extends OverlayMixin(LionInputDate) {
|
||||||
get _overlayInvokerNode() {
|
get _overlayInvokerNode() {
|
||||||
return this._invokerElement;
|
return this._invokerElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override Configures OverlayMixin
|
||||||
|
*/
|
||||||
|
get _overlayContentNode() {
|
||||||
|
return this.shadowRoot.querySelector('lion-calendar-overlay-frame');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,23 @@
|
||||||
|
|
||||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||||
|
|
||||||
Supports different types of overlays like dialogs, toasts, tooltips, dropdown, etc...
|
Supports different types of overlays like dialogs, toasts, tooltips, dropdown, etc.
|
||||||
|
|
||||||
Manages their position on the screen relative to other elements, including other overlays.
|
Manages their position on the screen relative to other elements, including other overlays.
|
||||||
|
|
||||||
|
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.
|
- 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 +31,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 .config=${{
|
||||||
/* options */
|
placementMode: 'global',
|
||||||
}),
|
viewportConfig: { placement: 'bottom-right' },
|
||||||
);
|
}}>
|
||||||
// name OverlayTypeController is for illustration purpose only
|
<div slot="content">
|
||||||
// please read below about existing classes for different types of overlays
|
This is an overlay
|
||||||
|
<button
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { 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.
|
|
||||||
191
packages/overlays/docs/OverlayController.md
Normal file
191
packages/overlays/docs/OverlayController.md
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
# 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].
|
||||||
|
- {Boolean} hidesOnHideEventInContentNode - (defaults to true) hides if an event called "hide" is fired within the content
|
||||||
|
- {Boolean} hidesOnOutsideClick - hides if user clicks outside of the overlay
|
||||||
|
- {Element} elementToFocusAfterHide - the element that should be called `.focus()` on after dialog closes.
|
||||||
|
- {Boolean} hasBackdrop - whether it should have a backdrop. (local mode only)
|
||||||
|
- {Boolean} isBlocking - hides other overlays when multiple are opened.
|
||||||
|
- {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.
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
"stories",
|
"stories",
|
||||||
"test",
|
"test",
|
||||||
"test-helpers",
|
"test-helpers",
|
||||||
|
"test-suites",
|
||||||
"translations",
|
"translations",
|
||||||
"*.js"
|
"*.js"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -33,6 +33,7 @@ export class OverlayController {
|
||||||
trapsKeyboardFocus: false,
|
trapsKeyboardFocus: false,
|
||||||
hidesOnEsc: false,
|
hidesOnEsc: false,
|
||||||
hidesOnOutsideClick: false,
|
hidesOnOutsideClick: false,
|
||||||
|
hidesOnHideEventInContentNode: true,
|
||||||
isTooltip: false,
|
isTooltip: false,
|
||||||
handlesUserInteraction: false,
|
handlesUserInteraction: false,
|
||||||
handlesAccessibility: false,
|
handlesAccessibility: false,
|
||||||
|
|
@ -152,7 +153,7 @@ export class OverlayController {
|
||||||
// TODO: Instead, prefetch it or use a preloader-manager to load it during idle time
|
// TODO: Instead, prefetch it or use a preloader-manager to load it during idle time
|
||||||
this.constructor.popperModule = preloadPopper();
|
this.constructor.popperModule = preloadPopper();
|
||||||
}
|
}
|
||||||
this.__mergePopperConfigs(this.popperConfig || {});
|
this.__mergePopperConfigs(cfgToAdd.popperConfig || {});
|
||||||
}
|
}
|
||||||
this._handleFeatures({ phase: 'init' });
|
this._handleFeatures({ phase: 'init' });
|
||||||
}
|
}
|
||||||
|
|
@ -167,6 +168,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 +300,9 @@ 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) {
|
||||||
|
this.elementToFocusAfterHide.focus();
|
||||||
|
}
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,6 +337,9 @@ export class OverlayController {
|
||||||
if (this.hidesOnOutsideClick) {
|
if (this.hidesOnOutsideClick) {
|
||||||
this._handleHidesOnOutsideClick({ phase });
|
this._handleHidesOnOutsideClick({ phase });
|
||||||
}
|
}
|
||||||
|
if (this.hidesOnHideEventInContentNode) {
|
||||||
|
this._handleHidesOnHideEventInContentNode({ phase });
|
||||||
|
}
|
||||||
if (this.handlesAccessibility) {
|
if (this.handlesAccessibility) {
|
||||||
this._handleAccessibility({ phase });
|
this._handleAccessibility({ phase });
|
||||||
}
|
}
|
||||||
|
|
@ -480,8 +487,26 @@ export class OverlayController {
|
||||||
if (phase === 'show') {
|
if (phase === 'show') {
|
||||||
this.__escKeyHandler = ev => ev.key === 'Escape' && this.hide();
|
this.__escKeyHandler = ev => ev.key === 'Escape' && this.hide();
|
||||||
this.contentNode.addEventListener('keyup', this.__escKeyHandler);
|
this.contentNode.addEventListener('keyup', this.__escKeyHandler);
|
||||||
|
if (this.invokerNode) {
|
||||||
|
this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
|
||||||
|
}
|
||||||
} else if (phase === 'hide') {
|
} else if (phase === 'hide') {
|
||||||
this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
|
this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
|
||||||
|
if (this.invokerNode) {
|
||||||
|
this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleHidesOnHideEventInContentNode({ phase }) {
|
||||||
|
if (phase === 'show') {
|
||||||
|
this.__hideEventInContentNodeHandler = ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.hide();
|
||||||
|
};
|
||||||
|
this.contentNode.addEventListener('hide', this.__hideEventInContentNodeHandler);
|
||||||
|
} else if (phase === 'hide') {
|
||||||
|
this.contentNode.removeEventListener('keyup', this.__hideEventInContentNodeHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,8 +565,7 @@ export class OverlayController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Popper does not export a nice method to update an existing instance with a new config. Therefore we recreate the instance.
|
// TODO: Remove when no longer required by OverlayMixin (after updateConfig works properly while opened)
|
||||||
// TODO: Send a merge request to Popper to abstract their logic in the constructor to an exposed method which takes in the user config.
|
|
||||||
async updatePopperConfig(config = {}) {
|
async updatePopperConfig(config = {}) {
|
||||||
this.__mergePopperConfigs(config);
|
this.__mergePopperConfigs(config);
|
||||||
if (this.isShown) {
|
if (this.isShown) {
|
||||||
|
|
@ -552,6 +576,7 @@ export class OverlayController {
|
||||||
|
|
||||||
teardown() {
|
teardown() {
|
||||||
this._handleFeatures({ phase: 'teardown' });
|
this._handleFeatures({ phase: 'teardown' });
|
||||||
|
this._contentNodeWrapper.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -585,14 +610,19 @@ export class OverlayController {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Deep merging default config, previously configured user config, new user config
|
/**
|
||||||
this.popperConfig = {
|
* Deep merging:
|
||||||
|
* - default config
|
||||||
|
* - previously configured user config
|
||||||
|
* - new user added config
|
||||||
|
*/
|
||||||
|
this.config.popperConfig = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
...(this.popperConfig || {}),
|
...(this.config.popperConfig || {}),
|
||||||
...(config || {}),
|
...(config || {}),
|
||||||
modifiers: {
|
modifiers: {
|
||||||
...defaultConfig.modifiers,
|
...defaultConfig.modifiers,
|
||||||
...((this.popperConfig && this.popperConfig.modifiers) || {}),
|
...((this.config.popperConfig && this.config.popperConfig.modifiers) || {}),
|
||||||
...((config && config.modifiers) || {}),
|
...((config && config.modifiers) || {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -605,7 +635,7 @@ export class OverlayController {
|
||||||
}
|
}
|
||||||
const { default: Popper } = await this.constructor.popperModule;
|
const { default: Popper } = await this.constructor.popperModule;
|
||||||
this._popper = new Popper(this._referenceNode, this._contentNodeWrapper, {
|
this._popper = new Popper(this._referenceNode, this._contentNodeWrapper, {
|
||||||
...this.popperConfig,
|
...this.config.popperConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { render, dedupeMixin } from '@lion/core';
|
import { dedupeMixin } from '@lion/core';
|
||||||
|
import { OverlayController } from './OverlayController.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Function()}
|
* @type {Function()}
|
||||||
|
|
@ -15,132 +16,165 @@ export const OverlayMixin = dedupeMixin(
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflect: true,
|
reflect: true,
|
||||||
},
|
},
|
||||||
popperConfig: Object,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get opened() {
|
constructor() {
|
||||||
return this._overlayCtrl.isShown;
|
super();
|
||||||
|
this.opened = false;
|
||||||
|
this.config = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
set opened(show) {
|
get config() {
|
||||||
this._opened = show; // mainly captured for sync on connectedCallback
|
return this.__config;
|
||||||
|
}
|
||||||
|
|
||||||
|
set config(value) {
|
||||||
if (this._overlayCtrl) {
|
if (this._overlayCtrl) {
|
||||||
this.__syncOpened();
|
this._overlayCtrl.updateConfig(value);
|
||||||
|
}
|
||||||
|
this.__config = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable method `_defineOverlay`
|
||||||
|
* @desc returns an instance of a (dynamic) overlay controller
|
||||||
|
* In case overriding _defineOverlayConfig is not enough
|
||||||
|
* @returns {OverlayController}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line
|
||||||
|
_defineOverlay({ contentNode, invokerNode }) {
|
||||||
|
return new OverlayController({
|
||||||
|
contentNode,
|
||||||
|
invokerNode,
|
||||||
|
...this._defineOverlayConfig(),
|
||||||
|
...this.config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable method `_defineOverlay`
|
||||||
|
* @desc returns an object with default configuration options for your overlay component.
|
||||||
|
* This is generally speaking easier to override than _defineOverlay method entirely.
|
||||||
|
* @returns {OverlayController}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return {
|
||||||
|
placementMode: 'local',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has('opened')) {
|
||||||
|
if (this._overlayCtrl) {
|
||||||
|
this.__syncToOverlayController();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
__syncOpened() {
|
/**
|
||||||
if (this._opened) {
|
* @overridable
|
||||||
|
* @desc use this method to setup your open and close event listeners
|
||||||
|
* For example, set a click event listener on _overlayInvokerNode to set opened to true
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_setupOpenCloseListeners() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @overridable
|
||||||
|
* @desc use this method to tear down your event listeners
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_teardownOpenCloseListeners() {}
|
||||||
|
|
||||||
|
firstUpdated(changedProperties) {
|
||||||
|
super.firstUpdated(changedProperties);
|
||||||
|
// we setup in firstUpdated so we can use nodes from light and shadowDom
|
||||||
|
this._setupOverlayCtrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (super.disconnectedCallback) {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
}
|
||||||
|
if (this._overlayCtrl) {
|
||||||
|
this._teardownOverlayCtrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get _overlayInvokerNode() {
|
||||||
|
return Array.from(this.children).find(child => child.slot === 'invoker');
|
||||||
|
}
|
||||||
|
|
||||||
|
get _overlayContentNode() {
|
||||||
|
let contentNode;
|
||||||
|
|
||||||
|
// FIXME: This should shadow outlet in between the host and the content slot, is a problem
|
||||||
|
// Should simply be Array.from(this.children).find(child => child.slot === 'content')
|
||||||
|
// Issue: https://github.com/ing-bank/lion/issues/382
|
||||||
|
const shadowOutlet = Array.from(this.children).find(
|
||||||
|
child => child.slot === '_overlay-shadow-outlet',
|
||||||
|
);
|
||||||
|
if (shadowOutlet) {
|
||||||
|
contentNode = Array.from(shadowOutlet.children).find(child => child.slot === 'content');
|
||||||
|
} else {
|
||||||
|
contentNode = Array.from(this.children).find(child => child.slot === 'content');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentNode) {
|
||||||
|
this._cachedOverlayContentNode = contentNode;
|
||||||
|
}
|
||||||
|
return contentNode || this._cachedOverlayContentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOverlayCtrl() {
|
||||||
|
this._overlayCtrl = this._defineOverlay({
|
||||||
|
contentNode: this._overlayContentNode,
|
||||||
|
invokerNode: this._overlayInvokerNode,
|
||||||
|
});
|
||||||
|
this.__syncToOverlayController();
|
||||||
|
this.__setupSyncFromOverlayController();
|
||||||
|
|
||||||
|
this._setupOpenCloseListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOverlayCtrl() {
|
||||||
|
this._teardownOpenCloseListeners();
|
||||||
|
this.__teardownSyncFromOverlayController();
|
||||||
|
this._overlayCtrl.teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
__setupSyncFromOverlayController() {
|
||||||
|
this.__onOverlayCtrlShow = () => {
|
||||||
|
this.opened = true;
|
||||||
|
};
|
||||||
|
this.__onOverlayCtrlHide = () => {
|
||||||
|
this.opened = false;
|
||||||
|
};
|
||||||
|
this.__onBeforeShow = () => {
|
||||||
|
this.dispatchEvent(new Event('before-show'));
|
||||||
|
};
|
||||||
|
|
||||||
|
this._overlayCtrl.addEventListener('show', this.__onOverlayCtrlShow);
|
||||||
|
this._overlayCtrl.addEventListener('hide', this.__onOverlayCtrlHide);
|
||||||
|
this._overlayCtrl.addEventListener('before-show', this.__onBeforeShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
__teardownSyncFromOverlayController() {
|
||||||
|
this._overlayCtrl.removeEventListener('show', this.__onOverlayCtrlShow);
|
||||||
|
this._overlayCtrl.removeEventListener('hide', this.__onOverlayCtrlHide);
|
||||||
|
this._overlayCtrl.removeEventListener('before-show', this.__onBeforeShow);
|
||||||
|
}
|
||||||
|
|
||||||
|
__syncToOverlayController() {
|
||||||
|
if (this.opened) {
|
||||||
this._overlayCtrl.show();
|
this._overlayCtrl.show();
|
||||||
} else {
|
} else {
|
||||||
this._overlayCtrl.hide();
|
this._overlayCtrl.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get popperConfig() {
|
|
||||||
return this._popperConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
set popperConfig(config) {
|
|
||||||
this._popperConfig = {
|
|
||||||
...this._popperConfig,
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
this.__syncPopper();
|
|
||||||
}
|
|
||||||
|
|
||||||
__syncPopper() {
|
|
||||||
if (this._overlayCtrl) {
|
|
||||||
this._overlayCtrl.updatePopperConfig(this._popperConfig);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
if (super.connectedCallback) {
|
|
||||||
super.connectedCallback();
|
|
||||||
}
|
|
||||||
this._createOverlay();
|
|
||||||
this.__syncOpened();
|
|
||||||
this.__syncPopper();
|
|
||||||
}
|
|
||||||
|
|
||||||
firstUpdated(c) {
|
|
||||||
super.firstUpdated(c);
|
|
||||||
this._createOutletForLocalOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
updated(c) {
|
|
||||||
super.updated(c);
|
|
||||||
if (this.__managesOverlayViaTemplate) {
|
|
||||||
this._renderOverlayContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderOverlayContent() {
|
|
||||||
render(this._overlayTemplate(), this.__contentParent, {
|
|
||||||
scopeName: this.localName,
|
|
||||||
eventContext: this,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @desc Two options for a Subclasser:
|
|
||||||
* - 1: Define a template in `._overlayTemplate`. In this case the overlay content is
|
|
||||||
* predefined and thus belongs to the web component. Examples: datepicker.
|
|
||||||
* - 2: Define a getter `_overlayContentNode` that returns a node reference to a (content
|
|
||||||
* projected) node. Used when Application Developer is in charge of the content. Examples:
|
|
||||||
* popover, dialog, bottom sheet, dropdown, tooltip, select, combobox etc.
|
|
||||||
*/
|
|
||||||
get __managesOverlayViaTemplate() {
|
|
||||||
return Boolean(this._overlayTemplate);
|
|
||||||
}
|
|
||||||
|
|
||||||
_createOverlay() {
|
|
||||||
let contentNode;
|
|
||||||
if (this.__managesOverlayViaTemplate) {
|
|
||||||
this.__contentParent = document.createElement('div');
|
|
||||||
this._renderOverlayContent();
|
|
||||||
contentNode = this.__contentParent.firstElementChild;
|
|
||||||
} else {
|
|
||||||
contentNode = this._overlayContentNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Why no template support for invokerNode?
|
|
||||||
// -> Because this node will always be managed by the Subclasser and should
|
|
||||||
// reside in the dom of the sub class. A reference to a rendered node suffices.
|
|
||||||
const invokerNode = this._overlayInvokerNode;
|
|
||||||
this._overlayCtrl = this._defineOverlay({ contentNode, invokerNode });
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: We add an overlay slot to the wrapper, but the content node already has a slot="content"
|
|
||||||
// This is a big problem, because slots should be direct children of its host element.
|
|
||||||
// Putting the shadow outlet slot in between breaks that. https://github.com/ing-bank/lion/issues/382
|
|
||||||
/**
|
|
||||||
* @desc Should be called by Subclasser for local overlay support in shadow roots
|
|
||||||
* Create an outlet slot in shadow dom that our local overlay can pass through
|
|
||||||
*/
|
|
||||||
_createOutletForLocalOverlay() {
|
|
||||||
const outlet = document.createElement('slot');
|
|
||||||
outlet.name = '_overlay-shadow-outlet';
|
|
||||||
this.shadowRoot.appendChild(outlet);
|
|
||||||
this._overlayCtrl._contentNodeWrapper.slot = '_overlay-shadow-outlet';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @overridable method `_overlayTemplate`
|
|
||||||
* Be aware that the overlay will be placed in a different shadow root.
|
|
||||||
* Therefore, style encapsulation should be provided by the contents of
|
|
||||||
* _overlayTemplate
|
|
||||||
* @return {TemplateResult}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @overridable method `_defineOverlay`
|
|
||||||
* @desc returns an instance of a (dynamic) overlay controller
|
|
||||||
* @returns {OverlayController}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line
|
|
||||||
_defineOverlay({ contentNode, invokerNode }) {}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,491 @@
|
||||||
import './global-overlay.stories.js';
|
import { storiesOf, html, withKnobs } from '@open-wc/demoing-storybook';
|
||||||
import './modal-dialog.stories.js';
|
import { css, render, LitElement } from '@lion/core';
|
||||||
import './bottom-sheet.stories.js';
|
import {
|
||||||
import './local-overlay.stories.js';
|
withBottomSheetConfig,
|
||||||
import './local-overlay-placement.stories.js';
|
withDropdownConfig,
|
||||||
import './dynamic-overlay.stories.js';
|
withModalDialogConfig,
|
||||||
|
OverlayMixin,
|
||||||
|
} from '../index.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-demo-overlay {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
color: black;
|
||||||
|
font-size: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-box__column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-overlay {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-overlay button {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-popup {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
customElements.define(
|
||||||
|
'lion-demo-overlay',
|
||||||
|
class extends OverlayMixin(LitElement) {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_defineOverlayConfig() {
|
||||||
|
return {
|
||||||
|
placementMode: 'global', // have to set a default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
|
this.__toggle = () => {
|
||||||
|
this.opened = !this.opened;
|
||||||
|
};
|
||||||
|
this._overlayInvokerNode.addEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
_teardownOpenCloseListeners() {
|
||||||
|
this._overlayInvokerNode.removeEventListener('click', this.__toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<slot name="_overlay-shadow-outlet"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
storiesOf('Overlay System | Overlay as a WC', module)
|
||||||
|
.addDecorator(withKnobs)
|
||||||
|
.add(
|
||||||
|
'Default',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<p>
|
||||||
|
Important note: For <code>placementMode: 'global'</code>, your
|
||||||
|
<code>slot="content"</code> gets moved to global overlay container. After initialization it
|
||||||
|
is no longer a child of <code>lion-demo-overlay</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To close your overlay from some action performed inside the content slot, fire a
|
||||||
|
<code>hide</code> event.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For the overlay to close, it will need to bubble to the content slot (use
|
||||||
|
<code>bubbles: true</code>. If absolutely needed <code>composed: true</code> can be used to
|
||||||
|
traverse shadow boundaries)
|
||||||
|
</p>
|
||||||
|
<p>The demo below demonstrates this</p>
|
||||||
|
<div class="demo-box">
|
||||||
|
<lion-demo-overlay>
|
||||||
|
<button slot="invoker">Overlay</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-demo-overlay>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Global placement configuration', () => {
|
||||||
|
const overlay = placement => html`
|
||||||
|
<lion-demo-overlay
|
||||||
|
.config=${{ hasBackdrop: true, trapsKeyboardFocus: true, viewportConfig: { placement } }}
|
||||||
|
>
|
||||||
|
<button slot="invoker">Overlay ${placement}</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-demo-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(
|
||||||
|
'Nested overlays',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<lion-demo-overlay .config=${{ ...withModalDialogConfig() }}>
|
||||||
|
<button slot="invoker">Overlay</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
<div>
|
||||||
|
Hello! This is a notification.
|
||||||
|
<button @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<lion-demo-overlay
|
||||||
|
.config=${{ ...withModalDialogConfig(), viewportConfig: { placement: 'top' } }}
|
||||||
|
>
|
||||||
|
<button slot="invoker">Open child</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-demo-overlay>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</lion-demo-overlay>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
'Local placementMode',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
<lion-demo-overlay
|
||||||
|
.config=${{ placementMode: 'local', popperConfig: { placement: 'bottom-start' } }}
|
||||||
|
>
|
||||||
|
<button slot="invoker">Overlay</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-demo-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-demo-overlay
|
||||||
|
.config=${{
|
||||||
|
placementMode: 'local',
|
||||||
|
hidesOnEsc: true,
|
||||||
|
hidesOnOutsideClick: true,
|
||||||
|
popperConfig: {
|
||||||
|
placement: 'bottom-end',
|
||||||
|
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-demo-overlay>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('Switch overlays configuration', () => {
|
||||||
|
const overlay = renderOffline(html`
|
||||||
|
<lion-demo-overlay .config=${{ ...withBottomSheetConfig() }}>
|
||||||
|
<button slot="invoker">Overlay</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-demo-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(
|
||||||
|
'Responsive switching',
|
||||||
|
() => html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<p>Open the overlay on a big screen and it will be a dialog</p>
|
||||||
|
<p>Close and open it again on a small screen (< 600px) and it will be a bottom sheet</p>
|
||||||
|
<lion-demo-overlay
|
||||||
|
.config=${{ ...withBottomSheetConfig() }}
|
||||||
|
@before-show=${e => {
|
||||||
|
if (window.innerWidth >= 600) {
|
||||||
|
e.target.config = { ...withModalDialogConfig() };
|
||||||
|
} else {
|
||||||
|
e.target.config = { ...withBottomSheetConfig() };
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button slot="invoker">Overlay</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>
|
||||||
|
⨯
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</lion-demo-overlay>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.add('On hover', () => {
|
||||||
|
const popup = renderOffline(html`
|
||||||
|
<lion-demo-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="demo-overlay">
|
||||||
|
United Kingdom
|
||||||
|
</div>
|
||||||
|
</lion-demo-overlay>
|
||||||
|
`);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
${overlayDemoStyle}
|
||||||
|
</style>
|
||||||
|
<div class="demo-box_placements">
|
||||||
|
<p>In the beautiful ${popup} the weather is nice.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.add('On an input', () => {
|
||||||
|
const popup = renderOffline(html`
|
||||||
|
<lion-demo-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"
|
||||||
|
autocomplete="off"
|
||||||
|
@click=${e => e.stopImmediatePropagation()}
|
||||||
|
@focusout=${() => {
|
||||||
|
popup.opened = false;
|
||||||
|
}}
|
||||||
|
@focusin=${() => {
|
||||||
|
popup.opened = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</lion-demo-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-demo-overlay
|
||||||
|
.config=${{
|
||||||
|
placementMode,
|
||||||
|
...(placementMode === 'global'
|
||||||
|
? { viewportConfig: { placement: text('global config', 'center') } }
|
||||||
|
: { popperConfig: { placement: text('local config', 'top-start') } }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button slot="invoker">Overlay</button>
|
||||||
|
<div slot="content" class="demo-overlay">
|
||||||
|
Hello! You can close this notification here:
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}
|
||||||
|
>⨯</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</lion-demo-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 Event('hide'))}">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,
|
||||||
|
|
@ -288,9 +265,6 @@ storiesOf('Global Overlay System|Global Overlay', module)
|
||||||
element.addEventListener('toggle-placement', e => {
|
element.addEventListener('toggle-placement', e => {
|
||||||
overlayCtrl.updateConfig({ viewportConfig: { placement: e.detail } });
|
overlayCtrl.updateConfig({ viewportConfig: { placement: e.detail } });
|
||||||
});
|
});
|
||||||
element.addEventListener('close', () => {
|
|
||||||
overlayCtrl.hide();
|
|
||||||
});
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
${globalOverlayDemoStyle}
|
${globalOverlayDemoStyle}
|
||||||
|
|
@ -300,7 +274,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(
|
||||||
57
packages/overlays/test-suites/OverlayMixin.suite.js
Normal file
57
packages/overlays/test-suites/OverlayMixin.suite.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { expect, fixture, html, aTimeout } from '@open-wc/testing';
|
||||||
|
|
||||||
|
export function runOverlayMixinSuite({ /* tagString, */ tag, suffix = '' }) {
|
||||||
|
describe(`OverlayMixin${suffix}`, () => {
|
||||||
|
let el;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
el = await fixture(html`
|
||||||
|
<${tag}>
|
||||||
|
<div slot="content">content of the overlay</div>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be opened by default', async () => {
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs opened to overlayController', async () => {
|
||||||
|
el.opened = true;
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
await aTimeout(); // overlayCtrl show/hide is async
|
||||||
|
expect(el._overlayCtrl.isShown).to.be.true;
|
||||||
|
|
||||||
|
el.opened = false;
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
await aTimeout(0); // overlayCtrl show/hide is async
|
||||||
|
expect(el._overlayCtrl.isShown).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs overlayController to opened', async () => {
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
await el._overlayCtrl.show();
|
||||||
|
expect(el.opened).to.be.true;
|
||||||
|
|
||||||
|
await el._overlayCtrl.hide();
|
||||||
|
expect(el.opened).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond to initially and dynamically setting the config', async () => {
|
||||||
|
const itEl = await fixture(html`
|
||||||
|
<${tag} .config=${{ trapsKeyboardFocus: false, viewportConfig: { placement: 'top' } }}>
|
||||||
|
<div slot="content">content of the overlay</div>
|
||||||
|
<button slot="invoker">invoker button</button>
|
||||||
|
</${tag}>
|
||||||
|
`);
|
||||||
|
itEl.opened = true;
|
||||||
|
await itEl.updateComplete;
|
||||||
|
expect(itEl._overlayCtrl.trapsKeyboardFocus).to.be.false;
|
||||||
|
|
||||||
|
itEl.config = { viewportConfig: { placement: 'left' } };
|
||||||
|
expect(itEl._overlayCtrl.viewportConfig.placement).to.equal('left');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -162,6 +162,19 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Add teardown feature tests
|
||||||
|
describe('Teardown', () => {
|
||||||
|
it('removes the contentNodeWrapper from global rootnode upon teardown', async () => {
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ctrl.manager.globalRootNode.children.length).to.equal(1);
|
||||||
|
ctrl.teardown();
|
||||||
|
expect(ctrl.manager.globalRootNode.children.length).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Node Configuration', () => {
|
describe('Node Configuration', () => {
|
||||||
it('accepts an .contentNode<Node> to directly set content', async () => {
|
it('accepts an .contentNode<Node> to directly set content', async () => {
|
||||||
const ctrl = new OverlayController({
|
const ctrl = new OverlayController({
|
||||||
|
|
@ -301,6 +314,33 @@ describe('OverlayController', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('hidesOnHideEventInContentNode', () => {
|
||||||
|
it('hides content on hide event within the content ', async () => {
|
||||||
|
const ctrl = new OverlayController({
|
||||||
|
...withGlobalTestConfig(),
|
||||||
|
hidesOnHideEventInContentNode: true,
|
||||||
|
contentNode: fixtureSync(html`
|
||||||
|
<div>
|
||||||
|
my content
|
||||||
|
<button @click=${e => e.target.dispatchEvent(new Event('hide', { bubbles: true }))}>
|
||||||
|
x
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`),
|
||||||
|
});
|
||||||
|
await ctrl.show();
|
||||||
|
|
||||||
|
const closeBtn = ctrl.contentNode.querySelector('button');
|
||||||
|
closeBtn.click();
|
||||||
|
|
||||||
|
expect(ctrl.isShown).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does stop propagation of the "hide" event to not pollute the event stack and to prevent side effects', () => {
|
||||||
|
// TODO: how to test this?
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('hidesOnOutsideClick', () => {
|
describe('hidesOnOutsideClick', () => {
|
||||||
it('hides on outside click', async () => {
|
it('hides on outside click', async () => {
|
||||||
const contentNode = await fixture('<div>Content</div>');
|
const contentNode = await fixture('<div>Content</div>');
|
||||||
|
|
@ -860,7 +900,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 +920,34 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Currently not working, enable again when we fix updateConfig
|
||||||
|
it.skip('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', () => {
|
||||||
|
|
|
||||||
12
packages/overlays/test/OverlayMixin.test.js
Normal file
12
packages/overlays/test/OverlayMixin.test.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineCE, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { LitElement } from '@lion/core';
|
||||||
|
import { runOverlayMixinSuite } from '../test-suites/OverlayMixin.suite.js';
|
||||||
|
import { OverlayMixin } from '../src/OverlayMixin.js';
|
||||||
|
|
||||||
|
const tagString = defineCE(class extends OverlayMixin(LitElement) {});
|
||||||
|
const tag = unsafeStatic(tagString);
|
||||||
|
|
||||||
|
runOverlayMixinSuite({
|
||||||
|
tagString,
|
||||||
|
tag,
|
||||||
|
});
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
# Popup
|
|
||||||
|
|
||||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
|
||||||
|
|
||||||
`lion-popup` is a component used for basic popups on click.
|
|
||||||
Its purpose is to show content appearing when the user clicks an invoker element with the cursor or with the keyboard.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Show content when clicking the invoker
|
|
||||||
- Use the position property to position the content popup relative to the invoker
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i --save @lion/popup
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
import '@lion/popup/lion-popup.js';
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```html
|
|
||||||
<lion-popup>
|
|
||||||
<div slot="content" class="tooltip">This is a popup<div>
|
|
||||||
<a slot="invoker" href="https://www.google.com/">
|
|
||||||
Popup on link
|
|
||||||
</a>
|
|
||||||
</lion-popup>
|
|
||||||
```
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { LionPopup } from './src/LionPopup.js';
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { LionPopup } from './src/LionPopup.js';
|
|
||||||
|
|
||||||
customElements.define('lion-popup', LionPopup);
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import { LitElement, html } from '@lion/core';
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
|
||||||
_defineOverlay() {
|
|
||||||
return new OverlayController({
|
|
||||||
placementMode: 'local',
|
|
||||||
contentNode: this._overlayContentNode,
|
|
||||||
invokerNode: this._overlayInvokerNode,
|
|
||||||
handlesAccessibility: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.__toggle = () => this._overlayCtrl.toggle();
|
|
||||||
this._overlayInvokerNode.addEventListener('click', this.__toggle);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._overlayInvokerNode.removeEventListener('click', this._toggle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
import { storiesOf, html, withKnobs, object, text } from '@open-wc/demoing-storybook';
|
|
||||||
import { css } from '@lion/core';
|
|
||||||
|
|
||||||
import '@lion/icon/lion-icon.js';
|
|
||||||
import '@lion/button/lion-button.js';
|
|
||||||
import '../lion-popup.js';
|
|
||||||
|
|
||||||
const popupDemoStyle = css`
|
|
||||||
.demo-box {
|
|
||||||
width: 200px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
border: 1px solid grey;
|
|
||||||
margin: 250px 0 0 250px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-box_placements {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 173px;
|
|
||||||
margin: 0 auto;
|
|
||||||
margin-top: 68px;
|
|
||||||
}
|
|
||||||
|
|
||||||
lion-popup {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo-box__column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
font-size: 16px;
|
|
||||||
color: white;
|
|
||||||
background-color: black;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.popup {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
storiesOf('Local Overlay System|Popup', module)
|
|
||||||
.addDecorator(withKnobs)
|
|
||||||
.add(
|
|
||||||
'Button popup',
|
|
||||||
() => html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box">
|
|
||||||
<lion-popup .popperConfig="${{ placement: 'top' }}">
|
|
||||||
<lion-button slot="invoker">Popup</lion-button>
|
|
||||||
<div slot="content" class="popup">Hello there!</div>
|
|
||||||
</lion-popup>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.add(
|
|
||||||
'placements',
|
|
||||||
() => html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<div class="demo-box_placements">
|
|
||||||
<lion-popup .popperConfig="${{ placement: 'top' }}">
|
|
||||||
<lion-button slot="invoker">Top</lion-button>
|
|
||||||
<div slot="content" class="popup">Its top placement</div>
|
|
||||||
</lion-popup>
|
|
||||||
<lion-popup .popperConfig="${{ placement: 'right' }}">
|
|
||||||
<lion-button slot="invoker">Right</lion-button>
|
|
||||||
<div slot="content" class="popup">Its right placement</div>
|
|
||||||
</lion-popup>
|
|
||||||
<lion-popup .popperConfig="${{ placement: 'bottom' }}">
|
|
||||||
<lion-button slot="invoker">Bottom</lion-button>
|
|
||||||
<div slot="content" class="popup">Its bottom placement</div>
|
|
||||||
</lion-popup>
|
|
||||||
<lion-popup .popperConfig="${{ placement: 'left' }}">
|
|
||||||
<lion-button slot="invoker">Left</lion-button>
|
|
||||||
<div slot="content" class="popup">Its left placement</div>
|
|
||||||
</lion-popup>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.add(
|
|
||||||
'Override popper configuration',
|
|
||||||
() => html`
|
|
||||||
<style>
|
|
||||||
${popupDemoStyle}
|
|
||||||
</style>
|
|
||||||
<p>Use the Storybook Knobs to dynamically change the popper configuration!</p>
|
|
||||||
<div class="demo-box">
|
|
||||||
<lion-popup
|
|
||||||
.popperConfig="${object('Popper Configuration', {
|
|
||||||
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: 16 /* when enabled, this is the viewport-margin for shifting/sliding */,
|
|
||||||
},
|
|
||||||
flip: {
|
|
||||||
boundariesElement: 'viewport',
|
|
||||||
padding: 4 /* viewport-margin for flipping on primary axis */,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
enabled: true,
|
|
||||||
offset: `0, 4px` /* horizontal and vertical margin (distance between popper and referenceElement) */,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})}"
|
|
||||||
>
|
|
||||||
<lion-button slot="invoker">${text('Invoker text', 'Click me!')}</lion-button>
|
|
||||||
<div slot="content" class="popup">${text('Content text', 'Hello, World!')}</div>
|
|
||||||
</lion-popup>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
);
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
|
||||||
|
|
||||||
import '../lion-popup.js';
|
|
||||||
|
|
||||||
describe('lion-popup', () => {
|
|
||||||
describe('Basic', () => {
|
|
||||||
it('should not be shown by default', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-popup>
|
|
||||||
<div slot="content" class="popup">Hey there</div>
|
|
||||||
<lion-button slot="invoker">Popup button</lion-button>
|
|
||||||
</lion-popup>
|
|
||||||
`);
|
|
||||||
expect(el._overlayCtrl.isShown).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should toggle to show content on click', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-popup>
|
|
||||||
<div slot="content" class="popup">Hey there</div>
|
|
||||||
<lion-button slot="invoker">Popup button</lion-button>
|
|
||||||
</lion-popup>
|
|
||||||
`);
|
|
||||||
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
|
||||||
invoker.click();
|
|
||||||
await el.updateComplete;
|
|
||||||
|
|
||||||
expect(el._overlayCtrl.isShown).to.be.true;
|
|
||||||
invoker.click();
|
|
||||||
await el.updateComplete;
|
|
||||||
expect(el._overlayCtrl.isShown).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support popup containing html when specified in popup content body', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-popup>
|
|
||||||
<div slot="content">This is Popup using <strong id="click_overlay">overlay</strong></div>
|
|
||||||
<lion-button slot="invoker">Popup button</lion-button>
|
|
||||||
</lion-popup>
|
|
||||||
`);
|
|
||||||
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
|
||||||
const event = new Event('click');
|
|
||||||
invoker.dispatchEvent(event);
|
|
||||||
await el.updateComplete;
|
|
||||||
expect(el.querySelector('strong')).to.not.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should respond to dynamically changing the popperConfig', async () => {
|
|
||||||
const el = await fixture(html`
|
|
||||||
<lion-popup>
|
|
||||||
<div slot="content" class="popup">Hey there</div>
|
|
||||||
<lion-button slot="invoker">Popup button</lion-button>
|
|
||||||
</lion-popup>
|
|
||||||
`);
|
|
||||||
await el._overlayCtrl.show();
|
|
||||||
expect(el._overlayCtrl._popper.options.placement).to.equal('top');
|
|
||||||
|
|
||||||
el.popperConfig = { placement: 'left' };
|
|
||||||
await el._overlayCtrl.show();
|
|
||||||
expect(el._overlayCtrl._popper.options.placement).to.equal('left');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { html, css, LitElement, SlotMixin } from '@lion/core';
|
import { html, css, LitElement, SlotMixin } from '@lion/core';
|
||||||
import { OverlayController, withDropdownConfig, OverlayMixin } from '@lion/overlays';
|
import { withDropdownConfig, OverlayMixin } from '@lion/overlays';
|
||||||
import { FormControlMixin, InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
|
import { FormControlMixin, InteractionStateMixin, FormRegistrarMixin } from '@lion/field';
|
||||||
import { ValidateMixin } from '@lion/validate';
|
import { ValidateMixin } from '@lion/validate';
|
||||||
import './differentKeyNamesShimIE.js';
|
import './differentKeyNamesShimIE.js';
|
||||||
|
|
@ -182,12 +182,6 @@ export class LionSelectRich extends OverlayMixin(
|
||||||
if (super.connectedCallback) {
|
if (super.connectedCallback) {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.__setupOverlay();
|
|
||||||
this.__setupInvokerNode();
|
|
||||||
this.__setupListboxNode();
|
|
||||||
|
|
||||||
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
|
@ -202,6 +196,12 @@ export class LionSelectRich extends OverlayMixin(
|
||||||
|
|
||||||
firstUpdated(c) {
|
firstUpdated(c) {
|
||||||
super.firstUpdated(c);
|
super.firstUpdated(c);
|
||||||
|
this.__setupOverlay();
|
||||||
|
this.__setupInvokerNode();
|
||||||
|
this.__setupListboxNode();
|
||||||
|
|
||||||
|
this._invokerNode.selectedElement = this.formElements[this.checkedIndex];
|
||||||
|
|
||||||
this.__toggleInvokerDisabled();
|
this.__toggleInvokerDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,6 +243,14 @@ export class LionSelectRich extends OverlayMixin(
|
||||||
return this.querySelector('[slot="input"]');
|
return this.querySelector('[slot="input"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
${this.labelTemplate()} ${this.helpTextTemplate()} ${this.inputGroupTemplate()}
|
||||||
|
${this.feedbackTemplate()}
|
||||||
|
<slot name="_overlay-shadow-outlet"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
updated(changedProps) {
|
updated(changedProps) {
|
||||||
super.updated(changedProps);
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
|
@ -599,12 +609,10 @@ export class LionSelectRich extends OverlayMixin(
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_defineOverlay({ invokerNode, contentNode } = {}) {
|
_defineOverlayConfig() {
|
||||||
return new OverlayController({
|
return {
|
||||||
...withDropdownConfig(),
|
...withDropdownConfig(),
|
||||||
contentNode,
|
};
|
||||||
invokerNode,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__setupOverlay() {
|
__setupOverlay() {
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@ describe('lion-select-rich', () => {
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
el._invokerNode.click();
|
el._invokerNode.click();
|
||||||
await el.updateComplete;
|
await aTimeout();
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -258,7 +258,7 @@ describe('lion-select-rich', () => {
|
||||||
</lion-select-rich>
|
</lion-select-rich>
|
||||||
`);
|
`);
|
||||||
el._invokerNode.click();
|
el._invokerNode.click();
|
||||||
await el.updateComplete;
|
await aTimeout();
|
||||||
expect(el.opened).to.be.true;
|
expect(el.opened).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ e invoker element is focused.
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm i --save @lion/popup
|
npm i --save @lion/tooltip
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
|
@ -28,7 +28,7 @@ import '@lion/tooltip/lion-tooltip.js';
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<lion-tooltip>
|
<lion-tooltip>
|
||||||
<div slot="content" class="tooltip">This is a popup<div>
|
<div slot="content">This is a tooltip<div>
|
||||||
<a slot="invoker" href="https://www.google.com/">
|
<a slot="invoker" href="https://www.google.com/">
|
||||||
Popup on link
|
Popup on link
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,9 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lion/core": "^0.3.0",
|
"@lion/core": "^0.3.0",
|
||||||
"@lion/overlays": "^0.6.4",
|
"@lion/overlays": "^0.6.4"
|
||||||
"@lion/popup": "^0.3.20"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lion/button": "^0.3.43",
|
|
||||||
"@lion/icon": "^0.2.9",
|
|
||||||
"@open-wc/demoing-storybook": "^0.2.0",
|
"@open-wc/demoing-storybook": "^0.2.0",
|
||||||
"@open-wc/testing": "^2.3.4"
|
"@open-wc/testing": "^2.3.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
import { LionPopup } from '@lion/popup';
|
import { OverlayMixin } from '@lion/overlays';
|
||||||
|
import { LitElement, html } from '@lion/core';
|
||||||
|
|
||||||
export class LionTooltip extends LionPopup {
|
export class LionTooltip extends OverlayMixin(LitElement) {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.closeEventName = 'tooltip-close';
|
||||||
this.mouseActive = false;
|
this.mouseActive = false;
|
||||||
this.keyActive = false;
|
this.keyActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
super.connectedCallback();
|
_defineOverlayConfig() {
|
||||||
this._overlayContentNode.setAttribute('role', 'tooltip');
|
return {
|
||||||
|
placementMode: 'local', // have to set a default
|
||||||
|
elementToFocusAfterHide: null,
|
||||||
|
hidesOnEsc: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupOpenCloseListeners() {
|
||||||
this.__resetActive = () => {
|
this.__resetActive = () => {
|
||||||
this.mouseActive = false;
|
this.mouseActive = false;
|
||||||
this.keyActive = false;
|
this.keyActive = false;
|
||||||
|
|
@ -19,26 +27,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 +57,24 @@ export class LionTooltip extends LionPopup {
|
||||||
this._overlayInvokerNode.addEventListener('focusout', this.__hideKey);
|
this._overlayInvokerNode.addEventListener('focusout', this.__hideKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
_teardownOpenCloseListeners() {
|
||||||
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<slot name="invoker"></slot>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
<slot name="_overlay-shadow-outlet"></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import { storiesOf, html, withKnobs, object, text } from '@open-wc/demoing-storybook';
|
import { storiesOf, html, withKnobs, object, text } from '@open-wc/demoing-storybook';
|
||||||
import { css } from '@lion/core';
|
import { css } from '@lion/core';
|
||||||
|
|
||||||
import '@lion/icon/lion-icon.js';
|
|
||||||
import '@lion/button/lion-button.js';
|
|
||||||
import '../lion-tooltip.js';
|
import '../lion-tooltip.js';
|
||||||
|
|
||||||
const tooltipDemoStyle = css`
|
const tooltipDemoStyle = css`
|
||||||
|
|
@ -32,7 +30,7 @@ const tooltipDemoStyle = css`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.demo-tooltip {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
@ -41,15 +39,9 @@ const tooltipDemoStyle = css`
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.tooltip {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
storiesOf('Local Overlay System|Tooltip', module)
|
storiesOf('Overlays Specific WC|Tooltip', module)
|
||||||
.addDecorator(withKnobs)
|
.addDecorator(withKnobs)
|
||||||
.add(
|
.add(
|
||||||
'Button tooltip',
|
'Button tooltip',
|
||||||
|
|
@ -58,9 +50,9 @@ storiesOf('Local Overlay System|Tooltip', module)
|
||||||
${tooltipDemoStyle}
|
${tooltipDemoStyle}
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-box">
|
<div class="demo-box">
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'right' }}>
|
<lion-tooltip .config=${{ popperConfig: { placement: 'right' } }}>
|
||||||
<lion-button slot="invoker">Tooltip</lion-button>
|
<button slot="invoker">Tooltip</button>
|
||||||
<div slot="content" class="tooltip">Hello there!</div>
|
<div slot="content" class="demo-tooltip">Hello there!</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
@ -72,21 +64,21 @@ storiesOf('Local Overlay System|Tooltip', module)
|
||||||
${tooltipDemoStyle}
|
${tooltipDemoStyle}
|
||||||
</style>
|
</style>
|
||||||
<div class="demo-box_placements">
|
<div class="demo-box_placements">
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'top' }}>
|
<lion-tooltip .config=${{ popperConfig: { placement: 'top' } }}>
|
||||||
<lion-button slot="invoker">Top</lion-button>
|
<button slot="invoker">Top</button>
|
||||||
<div slot="content" class="tooltip">Its top placement</div>
|
<div slot="content" class="demo-tooltip">Its top placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'right' }}>
|
<lion-tooltip .config=${{ popperConfig: { placement: 'right' } }}>
|
||||||
<lion-button slot="invoker">Right</lion-button>
|
<button slot="invoker">Right</button>
|
||||||
<div slot="content" class="tooltip">Its right placement</div>
|
<div slot="content" class="demo-tooltip">Its right placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'bottom' }}>
|
<lion-tooltip .config=${{ popperConfig: { placement: 'bottom' } }}>
|
||||||
<lion-button slot="invoker">Bottom</lion-button>
|
<button slot="invoker">Bottom</button>
|
||||||
<div slot="content" class="tooltip">Its bottom placement</div>
|
<div slot="content" class="demo-tooltip">Its bottom placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
<lion-tooltip .popperConfig=${{ placement: 'left' }}>
|
<lion-tooltip .config=${{ popperConfig: { placement: 'left' } }}>
|
||||||
<lion-button slot="invoker">Left</lion-button>
|
<button slot="invoker">Left</button>
|
||||||
<div slot="content" class="tooltip">Its left placement</div>
|
<div slot="content" class="demo-tooltip">Its left placement</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
@ -100,31 +92,33 @@ storiesOf('Local Overlay System|Tooltip', module)
|
||||||
<p>Use the Storybook Knobs to dynamically change the popper configuration!</p>
|
<p>Use the Storybook Knobs to dynamically change the popper configuration!</p>
|
||||||
<div class="demo-box_placements">
|
<div class="demo-box_placements">
|
||||||
<lion-tooltip
|
<lion-tooltip
|
||||||
.popperConfig="${object('Popper Configuration', {
|
.config="${{
|
||||||
placement: 'bottom-start',
|
popperConfig: object('Popper Configuration', {
|
||||||
positionFixed: true,
|
placement: 'bottom-start',
|
||||||
modifiers: {
|
positionFixed: true,
|
||||||
keepTogether: {
|
modifiers: {
|
||||||
enabled: true /* Prevents detachment of content element from reference element */,
|
keepTogether: {
|
||||||
|
enabled: true /* Prevents detachment of content element from reference element */,
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: false /* disables shifting/sliding behavior on secondary axis */,
|
||||||
|
boundariesElement: 'viewport',
|
||||||
|
padding: 16 /* when enabled, this is the viewport-margin for shifting/sliding */,
|
||||||
|
},
|
||||||
|
flip: {
|
||||||
|
boundariesElement: 'viewport',
|
||||||
|
padding: 4 /* viewport-margin for flipping on primary axis */,
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
enabled: true,
|
||||||
|
offset: `0, 4px` /* horizontal and vertical margin (distance between popper and referenceElement) */,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
preventOverflow: {
|
}),
|
||||||
enabled: false /* disables shifting/sliding behavior on secondary axis */,
|
}}"
|
||||||
boundariesElement: 'viewport',
|
|
||||||
padding: 16 /* when enabled, this is the viewport-margin for shifting/sliding */,
|
|
||||||
},
|
|
||||||
flip: {
|
|
||||||
boundariesElement: 'viewport',
|
|
||||||
padding: 4 /* viewport-margin for flipping on primary axis */,
|
|
||||||
},
|
|
||||||
offset: {
|
|
||||||
enabled: true,
|
|
||||||
offset: `0, 4px` /* horizontal and vertical margin (distance between popper and referenceElement) */,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})}"
|
|
||||||
>
|
>
|
||||||
<lion-button slot="invoker">${text('Invoker text', 'Hover me!')}</lion-button>
|
<button slot="invoker">${text('Invoker text', 'Hover me!')}</button>
|
||||||
<div slot="content" class="tooltip">${text('Content text', 'Hello, World!')}</div>
|
<div slot="content" class="demo-tooltip">${text('Content text', 'Hello, World!')}</div>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,26 @@
|
||||||
import { expect, fixture, html } from '@open-wc/testing';
|
import { expect, fixture, html, unsafeStatic } from '@open-wc/testing';
|
||||||
|
import { runOverlayMixinSuite } from '@lion/overlays/test-suites/OverlayMixin.suite.js';
|
||||||
|
|
||||||
import '../lion-tooltip.js';
|
import '../lion-tooltip.js';
|
||||||
|
|
||||||
describe('lion-tooltip', () => {
|
describe('lion-tooltip', () => {
|
||||||
describe('Basic', () => {
|
describe('Integration tests', () => {
|
||||||
it('should not be shown by default', async () => {
|
const tagString = 'lion-tooltip';
|
||||||
const el = await fixture(html`
|
const tag = unsafeStatic(tagString);
|
||||||
<lion-tooltip>
|
|
||||||
<div slot="content">Hey there</div>
|
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
|
||||||
</lion-tooltip>
|
|
||||||
`);
|
|
||||||
expect(el._overlayCtrl.isShown).to.equal(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
runOverlayMixinSuite({
|
||||||
|
tagString,
|
||||||
|
tag,
|
||||||
|
suffix: ' for lion-tooltip',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Basic', () => {
|
||||||
it('should show content on mouseenter and hide on mouseleave', async () => {
|
it('should show content on mouseenter and hide on mouseleave', async () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-tooltip>
|
<lion-tooltip>
|
||||||
<div slot="content">Hey there</div>
|
<div slot="content">Hey there</div>
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<button slot="invoker">Tooltip button</button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
const eventMouseEnter = new Event('mouseenter');
|
const eventMouseEnter = new Event('mouseenter');
|
||||||
|
|
@ -35,7 +37,7 @@ describe('lion-tooltip', () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-tooltip>
|
<lion-tooltip>
|
||||||
<div slot="content">Hey there</div>
|
<div slot="content">Hey there</div>
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<button slot="invoker">Tooltip button</button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
const eventMouseEnter = new Event('mouseenter');
|
const eventMouseEnter = new Event('mouseenter');
|
||||||
|
|
@ -52,7 +54,7 @@ describe('lion-tooltip', () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-tooltip>
|
<lion-tooltip>
|
||||||
<div slot="content">Hey there</div>
|
<div slot="content">Hey there</div>
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<button slot="invoker">Tooltip button</button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
||||||
|
|
@ -70,7 +72,7 @@ describe('lion-tooltip', () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-tooltip>
|
<lion-tooltip>
|
||||||
<div slot="content">Hey there</div>
|
<div slot="content">Hey there</div>
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<button slot="invoker">Tooltip button</button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
||||||
|
|
@ -90,7 +92,7 @@ describe('lion-tooltip', () => {
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
This is Tooltip using <strong id="click_overlay">overlay</strong>
|
This is Tooltip using <strong id="click_overlay">overlay</strong>
|
||||||
</div>
|
</div>
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<button slot="invoker">Tooltip button</button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
const invoker = Array.from(el.children).find(child => child.slot === 'invoker');
|
||||||
|
|
@ -106,7 +108,7 @@ describe('lion-tooltip', () => {
|
||||||
const el = await fixture(html`
|
const el = await fixture(html`
|
||||||
<lion-tooltip>
|
<lion-tooltip>
|
||||||
<div slot="content">Hey there</div>
|
<div slot="content">Hey there</div>
|
||||||
<lion-button slot="invoker">Tooltip button</lion-button>
|
<button slot="invoker">Tooltip button</button>
|
||||||
</lion-tooltip>
|
</lion-tooltip>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,11 +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/overlays/stories/index.stories.js';
|
|
||||||
import '../packages/popup/stories/index.stories.js';
|
|
||||||
import '../packages/tooltip/stories/index.stories.js';
|
|
||||||
import '../packages/calendar/stories/index.stories.js';
|
import '../packages/calendar/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/tooltip/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