feat(overlays): simplify backdrop API, allow animated backdrop for local
This commit is contained in:
parent
b5e7df3ed9
commit
d83f7fc5ce
7 changed files with 213 additions and 122 deletions
12
.changeset/cool-panthers-design.md
Normal file
12
.changeset/cool-panthers-design.md
Normal 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.
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
48
packages/overlays/docs/demo-overlay-backdrop.js
Normal file
48
packages/overlays/docs/demo-overlay-backdrop.js
Normal 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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue