feat(overlays): simplify backdrop API, allow animated backdrop for local

This commit is contained in:
Joren Broekema 2020-10-08 11:32:03 +02:00
parent b5e7df3ed9
commit d83f7fc5ce
7 changed files with 213 additions and 122 deletions

View file

@ -0,0 +1,12 @@
---
'@lion/overlays': minor
---
⚠️ BREAKING CHANGE:
Fixed a few problems with the backdropNode and contentNode where they were not torn down properly.
Simplified the handleBackdrop code to work for both local and global.
Local overlay backdrop can now be animated.
Added more demos and docs for backdrop.
The breaking changes lie in the fact that the backdrop style classes are now prefixed 'local'/'global' respectively, instead of always 'global' and the class name suffixes for the `backdropNode` have changed from `fade-in`/`fade-out` to `animation-in`/`animation-out`, as not all animations are fades.

View file

@ -5,6 +5,7 @@
```js script
import { html } from 'lit-html';
import { render, LitElement } from '@lion/core';
import { renderLitAsNode } from '@lion/helpers';
import {
ArrowMixin,
OverlayMixin,
@ -14,6 +15,7 @@ import {
} from '../index.js';
import '../docs/demo-overlay-system.js';
import '../docs/demo-overlay-backdrop.js';
import '../docs/applyDemoOverlayStyles.js';
import { ref as r } from '../docs/directives/ref.js';
@ -29,7 +31,7 @@ On top of this, the system was built having accessibility in mind.
For a detailed rationale, please consult [Rationale](/?path=/docs/overlays-system-rationale--page).
```js story
```js preview-story
export const main = () => html`
<demo-overlay-system>
<button slot="invoker">Click me to open the overlay!</button>
@ -151,6 +153,100 @@ or declaratively in your template with the `.config` property
</demo-overlay-system>
```
### Backdrop
There are multiple ways to pass a backdrop node for your overlay.
The easiest way is declarative. This can be achieved by adding a `<slot name="backdrop">` to your `render` method. The component user can then insert a backdrop slottable declaratively:
```js preview-story
export const backdrop = () => html`
<demo-overlay-system .config=${{ ...withModalDialogConfig() }}>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}>
</button>
</div>
</demo-overlay-system>
`;
```
You can also pass a backdropNode imperatively to the OverlayConfig. Either use `hasBackdrop: true`, which will spawn a default `backdropNode`.
For more control, pass a `backdropNode` yourself, e.g. a webcomponent so you can easily encapsulate styles.
```js preview-story
export const backdropImperative = () => {
const backdropNode = document.createElement('demo-overlay-backdrop');
return html`
<demo-overlay-system .config=${{ ...withModalDialogConfig(), backdropNode }}>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</demo-overlay-system>
`;
};
```
#### Backdrop animation
By default our overlay system comes with a backdrop animation.
This will add `global-overlays__backdrop--animation-in` and `global-overlays__backdrop--animation-out` classes to your backdrop node.
If you have `placementMode: 'local'` it will replace those `global` strings in the CSS classes with `local`.
It expects from you that you act on these classes in your CSS with an animation. For example if you have your own backdrop webcomponent (to encapsulate styles):
```css
:host(.local-overlays__backdrop--animation-in) {
animation: local-overlays-backdrop-fade-in 300ms;
}
:host(.local-overlays__backdrop--animation-out) {
animation: local-overlays-backdrop-fade-out 300ms;
opacity: 0;
}
@keyframes local-overlays-backdrop-fade-in {
from {
opacity: 0;
}
}
@keyframes local-overlays-backdrop-fade-out {
from {
opacity: 0.3;
}
}
```
Under the hood, the OverlayController listens to `animationend` event, only then it will remove the animation-out/animation-in classes.
> If you don't intend on having a backdrop animation at all, as a subclasser you should override `transitionHide` and `transitionShow` OverlayMixin methods.
> Otherwise the `hide` will await an `animationend` event that will never happen.
```js preview-story
export const backdropAnimation = () => html`
<demo-overlay-system .config=${{ ...withModalDialogConfig() }}>
<button slot="invoker">Click me to open the overlay!</button>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}>
</button>
</div>
</demo-overlay-system>
`;
```
### Responsive switching
Currently we support switching between overlay configurations.
@ -174,12 +270,6 @@ export const responsiveSwitching = () => html`
}}
>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button @click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}>
</button>
</div>
</demo-overlay-system>
`;
```
@ -454,73 +544,9 @@ Here is the example below
```js preview-story
export const localBackdrop = () => {
let backdropNode = document.createElement('div');
backdropNode.classList.add('local-backdrop-01');
return html`
<style>
.local-backdrop-01 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
background-color: red;
opacity: 0.3;
display: none;
}
</style>
<demo-overlay-system
@before-opened=${e => (backdropNode.style.display = 'block')}
@before-closed=${e => (backdropNode.style.display = 'none')}
.config=${{ hasBackdrop: true, placementMode: 'local', backdropNode }}
>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:
<button
@click=${e => e.target.dispatchEvent(new Event('close-overlay', { bubbles: true }))}
>
</button>
</div>
</demo-overlay-system>
`;
};
```
## Declarative Local Backdrop
Another way to add custom backdrop is declaratively add an element with `slot="backdrop"`.
```js preview-story
export const declarativeLocalBackdrop = () => {
const beforeOpened = () => {
document.querySelector('.local-backdrop-02').style.display = 'block';
};
const beforeClosed = () => {
document.querySelector('.local-backdrop-02').style.display = 'none';
};
return html`
<style>
.local-backdrop-02 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
background-color: red;
opacity: 0.3;
display: none;
}
</style>
<demo-overlay-system
@before-opened=${beforeOpened}
@before-closed=${beforeClosed}
.config=${{ hasBackdrop: true, placementMode: 'local' }}
>
<div slot="backdrop" class="local-backdrop-02"></div>
<demo-overlay-system .config=${{ placementMode: 'local' }}>
<demo-overlay-backdrop slot="backdrop"></demo-overlay-backdrop>
<button slot="invoker">Click me to open the overlay!</button>
<div slot="content" class="demo-overlay">
Hello! You can close this notification here:

View file

@ -203,8 +203,8 @@ Boolean property. When true, will add a backdrop when the overlay is opened.
The backdrop styling can be configured by targeting the `.global-overlays .global-overlays__backdrop` css selector.
The backdrop animation can be configured by targeting the
`.global-overlays .global-overlays__backdrop--fade-in` and
`.global-overlays .global-overlays__backdrop--fade-out` css selector.
`.global-overlays .global-overlays__backdrop--animation-in` and
`.global-overlays .global-overlays__backdrop--animation-out` css selector.
This currently only supports CSS Animations, because it relies on the `animationend` event to add/remove classes.
```js preview-story
@ -235,8 +235,8 @@ Boolean property. When true, will add a backdrop when the overlay is opened.
The backdrop styling can be configured by targeting the `.global-overlays .global-overlays__backdrop` css selector.
The backdrop animation can be configured by targeting the
`.global-overlays .global-overlays__backdrop--fade-in` and
`.global-overlays .global-overlays__backdrop--fade-out` css selector.
`.global-overlays .global-overlays__backdrop--animation-in` and
`.global-overlays .global-overlays__backdrop--animation-out` css selector.
This currently only supports CSS Animations, because it relies on the `animationend` event to add/remove classes.
```js preview-story

View file

@ -0,0 +1,48 @@
import { css, LitElement } from '@lion/core';
/**
* @typedef {import('../types/OverlayConfig').OverlayConfig} OverlayConfig
*/
class DemoOverlayBackdrop extends LitElement {
static get styles() {
return css`
:host {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
background-color: red;
opacity: 0.3;
display: none;
}
:host(.local-overlays__backdrop--visible) {
display: block;
}
:host(.local-overlays__backdrop--animation-in) {
animation: local-overlays-backdrop-fade-in 300ms;
}
:host(.local-overlays__backdrop--animation-out) {
animation: local-overlays-backdrop-fade-out 300ms;
opacity: 0;
}
@keyframes local-overlays-backdrop-fade-in {
from {
opacity: 0;
}
}
@keyframes local-overlays-backdrop-fade-out {
from {
opacity: 0.3;
}
}
`;
}
}
customElements.define('demo-overlay-backdrop', DemoOverlayBackdrop);

View file

@ -40,7 +40,7 @@ class DemoOverlaySystem extends OverlayMixin(LitElement) {
render() {
return html`
<slot name="invoker"></slot>
<slot name="_overlay-shadow-outlet"></slot>
<slot name="backdrop"></slot>
<div id="overlay-content-node-wrapper">
<slot name="content"></slot>
</div>

View file

@ -251,7 +251,7 @@ export class OverlayController extends EventTargetShim {
* @type {boolean}
*/
get hasBackdrop() {
return /** @type {boolean} */ (this.config?.hasBackdrop);
return /** @type {boolean} */ (!!this.backdropNode || this.config?.hasBackdrop);
}
/**
@ -618,7 +618,7 @@ export class OverlayController extends EventTargetShim {
}
}
} else if (phase === 'teardown') {
this.__restorOriginalAttrs();
this.__restoreOriginalAttrs();
}
}
@ -634,7 +634,7 @@ export class OverlayController extends EventTargetShim {
this.__originalAttrs.set(node, attrMap);
}
__restorOriginalAttrs() {
__restoreOriginalAttrs() {
for (const [node, attrMap] of this.__originalAttrs) {
Object.entries(attrMap).forEach(([attrName, value]) => {
if (value !== null) {
@ -798,16 +798,49 @@ export class OverlayController extends EventTargetShim {
}
/**
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} config
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} hideConfig
*/
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async transitionHide(config) {}
async transitionHide(hideConfig) {
if (hideConfig.backdropNode) {
hideConfig.backdropNode.classList.remove(
`${this.placementMode}-overlays__backdrop--animation-in`,
);
/** @type {(ev:AnimationEvent) => void} */
let afterFadeOut;
hideConfig.backdropNode.classList.add(
`${this.placementMode}-overlays__backdrop--animation-out`,
);
this.__backdropAnimation = new Promise(resolve => {
afterFadeOut = () => {
if (hideConfig.backdropNode) {
hideConfig.backdropNode.classList.remove(
`${this.placementMode}-overlays__backdrop--animation-out`,
);
hideConfig.backdropNode.classList.remove(
`${this.placementMode}-overlays__backdrop--visible`,
);
hideConfig.backdropNode.removeEventListener('animationend', afterFadeOut);
}
resolve();
};
});
// @ts-expect-error
hideConfig.backdropNode.addEventListener('animationend', afterFadeOut);
}
}
/**
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} config
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} showConfig
*/
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async transitionShow(config) {}
async transitionShow(showConfig) {
if (showConfig.backdropNode) {
showConfig.backdropNode.classList.add(
`${this.placementMode}-overlays__backdrop--animation-in`,
);
}
}
_restoreFocus() {
// We only are allowed to move focus if we (still) 'own' it.
@ -893,19 +926,20 @@ export class OverlayController extends EventTargetShim {
/**
* Sets up backdrop on the given overlay. If there was a backdrop on another element
* it is removed. Otherwise this is the first time displaying a backdrop, so a fade-in
* it is removed. Otherwise this is the first time displaying a backdrop, so a animation-in
* animation is played.
* @param {{ animation?: boolean, phase: OverlayPhase }} config
*/
_handleBackdrop({ animation = true, phase }) {
_handleBackdrop({ phase }) {
switch (phase) {
case 'init': {
if (!this.backdropNode) {
this.__backdropNode = document.createElement('div');
/** @type {HTMLElement} */
(this.backdropNode).slot = 'backdrop';
/** @type {HTMLElement} */
(this.backdropNode).classList.add(`${this.placementMode}-overlays__backdrop`);
}
this.backdropNode.slot = '_overlay-shadow-outlet';
let insertionAnchor = /** @type {HTMLElement} */ (this.contentNode.parentNode);
let insertionBefore = this.contentNode;
@ -917,12 +951,7 @@ export class OverlayController extends EventTargetShim {
break;
}
case 'show':
if (this.placementMode === 'global') {
this.backdropNode.classList.add('global-overlays__backdrop--visible');
if (animation === true) {
this.backdropNode.classList.add('global-overlays__backdrop--fade-in');
}
}
this.backdropNode.classList.add(`${this.placementMode}-overlays__backdrop--visible`);
this.__hasActiveBackdrop = true;
break;
case 'hide':
@ -930,39 +959,15 @@ export class OverlayController extends EventTargetShim {
return;
}
this.__hasActiveBackdrop = false;
if (this.placementMode === 'global') {
this.backdropNode.classList.remove('global-overlays__backdrop--fade-in');
if (animation) {
/** @type {(ev:AnimationEvent) => void} */
let afterFadeOut;
this.backdropNode.classList.add('global-overlays__backdrop--fade-out');
this.__backDropAnimation = new Promise(resolve => {
afterFadeOut = () => {
if (this.backdropNode) {
this.backdropNode.classList.remove('global-overlays__backdrop--fade-out');
this.backdropNode.classList.remove('global-overlays__backdrop--visible');
this.backdropNode.removeEventListener('animationend', afterFadeOut);
}
resolve();
};
});
// @ts-expect-error
this.backdropNode.addEventListener('animationend', afterFadeOut);
} else {
this.backdropNode.classList.remove('global-overlays__backdrop--visible');
}
}
break;
case 'teardown':
if (!this.backdropNode || !this.backdropNode.parentNode) {
return;
}
if (animation && this.__backDropAnimation) {
if (this.__backdropAnimation) {
this.__backdropNodeToBeTornDown = this.backdropNode;
this.__backDropAnimation.then(() => {
this.__backdropAnimation.then(() => {
if (this.__backdropNodeToBeTornDown) {
/** @type {HTMLElement} */ (this.__backdropNodeToBeTornDown.parentNode).removeChild(
this.__backdropNodeToBeTornDown,

View file

@ -85,11 +85,11 @@ export const globalOverlaysStyle = css`
display: block;
}
.global-overlays .global-overlays__backdrop--fade-in {
.global-overlays .global-overlays__backdrop--animation-in {
animation: global-overlays-backdrop-fade-in 300ms;
}
.global-overlays .global-overlays__backdrop--fade-out {
.global-overlays .global-overlays__backdrop--animation-out {
animation: global-overlays-backdrop-fade-out 300ms;
opacity: 0;
}