diff --git a/.changeset/cool-panthers-design.md b/.changeset/cool-panthers-design.md new file mode 100644 index 000000000..31dee4523 --- /dev/null +++ b/.changeset/cool-panthers-design.md @@ -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. diff --git a/packages/overlays/docs/20-index.md b/packages/overlays/docs/20-index.md index c082dbae2..1ddce5050 100644 --- a/packages/overlays/docs/20-index.md +++ b/packages/overlays/docs/20-index.md @@ -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` @@ -151,6 +153,100 @@ or declaratively in your template with the `.config` property ``` +### 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 `` to your `render` method. The component user can then insert a backdrop slottable declaratively: + +```js preview-story +export const backdrop = () => html` + + + +
+ Hello! You can close this notification here: + +
+
+`; +``` + +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` + + +
+ Hello! You can close this notification here: + +
+
+ `; +}; +``` + +#### 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` + + + +
+ Hello! You can close this notification here: + +
+
+`; +``` + ### Responsive switching Currently we support switching between overlay configurations. @@ -174,12 +270,6 @@ export const responsiveSwitching = () => html` }} > -
- Hello! You can close this notification here: - -
`; ``` @@ -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` - - (backdropNode.style.display = 'block')} - @before-closed=${e => (backdropNode.style.display = 'none')} - .config=${{ hasBackdrop: true, placementMode: 'local', backdropNode }} - > - -
- Hello! You can close this notification here: - -
-
- `; -}; -``` - -## 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` - - -
+ +
Hello! You can close this notification here: diff --git a/packages/overlays/docs/40-system-configuration.md b/packages/overlays/docs/40-system-configuration.md index 17efe6209..4df796fef 100644 --- a/packages/overlays/docs/40-system-configuration.md +++ b/packages/overlays/docs/40-system-configuration.md @@ -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 diff --git a/packages/overlays/docs/demo-overlay-backdrop.js b/packages/overlays/docs/demo-overlay-backdrop.js new file mode 100644 index 000000000..3aa56e3b4 --- /dev/null +++ b/packages/overlays/docs/demo-overlay-backdrop.js @@ -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); diff --git a/packages/overlays/docs/demo-overlay-system.js b/packages/overlays/docs/demo-overlay-system.js index 4bcbfa85f..d94ae5499 100644 --- a/packages/overlays/docs/demo-overlay-system.js +++ b/packages/overlays/docs/demo-overlay-system.js @@ -40,7 +40,7 @@ class DemoOverlaySystem extends OverlayMixin(LitElement) { render() { return html` - +
diff --git a/packages/overlays/src/OverlayController.js b/packages/overlays/src/OverlayController.js index 9eca844a1..22f69a2f5 100644 --- a/packages/overlays/src/OverlayController.js +++ b/packages/overlays/src/OverlayController.js @@ -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, diff --git a/packages/overlays/src/globalOverlaysStyle.js b/packages/overlays/src/globalOverlaysStyle.js index d112f77fb..7325d1305 100644 --- a/packages/overlays/src/globalOverlaysStyle.js +++ b/packages/overlays/src/globalOverlaysStyle.js @@ -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; }