Skip to content

Instantly share code, notes, and snippets.

@SaladHut
Last active July 17, 2022 11:55
Show Gist options
  • Save SaladHut/222fd9694601412736ff2829c64b98dc to your computer and use it in GitHub Desktop.
Save SaladHut/222fd9694601412736ff2829c64b98dc to your computer and use it in GitHub Desktop.
Toast example made with litElement

Toast webcomponent example, MLIResume is in another gist. Substitute with your own choice of Promise solution. You can pop any lit html-tagged template literal or DOM element as long as it know to remove() itself.

  @query('#toaster') toaster;

  _testToast( title, body, icon, col, bg, timeout = 0 ){
    const toastRequest = new CustomEvent(
      'toast-notification',
      {
        bubbles: true,
        detail: {
          toast: html`
            <mli-toast
              style=${ styleMap({
                "--mli-theme-primary": bg,
                "--mli-theme-on-primary": col,
              })}
              timeout=${ timeout }
              icon=${ icon }
              title=${ title }
            >${ body }</mli-toast>
          `,
        }
      }
    );

    window.dispatchEvent( toastRequest );
  }

  _handleToastNotification = e => {
    this.toaster.pop( e.detail.toast );
  }
  
  connectedCallback(){
    super.connectedCallback();
    window.addEventListener('toast-notification', this._handleToastNotification );
  }
import { LitElement, html, css, unsafeCSS } from 'lit';
import { state, property, customElement, queryAssignedElements } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import dayjs from 'dayjs';
import '@material/mwc-icon';
import { CircularProgress } from '@material/mwc-circular-progress';
import { pageStyles } from './mli-styles.js';
import { sleep } from './functions';
// TODO action callbacks
@customElement('mli-circular-progress')
export class MLICircularProgress extends CircularProgress {
static get styles() {
return [ CircularProgress.styles, css`
.mdc-circular-progress__determinate-circle {
stroke-width: 2;
}
`];
}
}
@customElement('mli-toast')
export class MLIToast extends LitElement {
static get styles() {
return [
pageStyles,
css`
:host {
display: flex;
position: relative;
flex-flow: column;
color: var(--mli-theme-on-primary, white );
padding: 0.5em 1em 0.5em 0.5em;
margin-right: -0.5em;
gap: 0.2em;
background-color: var(--mli-theme-primary, rgba(0, 128, 128, 0.753 ));
box-shadow: var(--mdc-button-raised-box-shadow, 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12));
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
:host(:not(.placed)) {
animation: new-toast 1s ease-out;
}
@keyframes new-toast {
from {
transform: translateX(100%);
max-height: 0;
}
25% {
max-height: 100vh;
transform: translateX(100%);
opacity: 0;
}
50% {
transform: translateX(0);
}
to {
opacity: 1;
}
}
:host([showCloseButton]){
padding-left: 3em;
}
.header {
--mli-icon-size: 1.3em;
border-bottom: 1px solid currentColor;
padding-bottom: 0.1em;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.1em;
}
::slotted(*) {
font-size: 0.8em;
--mdc-theme-primary: currentColor;
--mdc-theme-on-primary: transparent;
}
::slotted(:empty) {
display: none;
}
footer {
display: flex;
justify-content: space-evenly
}
.close-button {
position: absolute;
left: 0px;
top: 50%;
transform: translateY(-50%);
}
mli-circular-progress {
--mdc-theme-primary: currentColor;
}
`];
}
@property() title;
@property() icon;
// @property({ type: Boolean, reflect: true }) closed = false;
_handlePrimaryAction( e ){
this.close();
}
_handleSecondaryAction( e ){
}
@queryAssignedElements({ slot:'primaryAction' }) _primaryList;
@property({ type: Boolean, reflect: true }) showCloseButton = false;
firstUpdated(){
if( !this._primaryList.length ){
this.showCloseButton = true;
}
setTimeout(() => this.classList.add('placed'), 1000 );
}
async close(){
if( this.__closing ) return;
this.__closing = true;
const subRect = this.getBoundingClientRect();
const elWidth = getComputedStyle( this ).width;
const sub = document.createElement('div');
sub.style.width = '0px';
sub.style.height = subRect.height+'px';
sub.style.borderRight = '3px dotted white';
sub.style.position = 'relative';
sub.style.zIndex = '-1';
this.parentNode.replaceChild( sub, this );
this.style.position = 'absolute';
this.style.top = '50%';
this.style.transform = 'translateY(-50%)';
this.style.right = '-3px';
this.style.width = elWidth;
sub.appendChild( this );
sub.animate({
height:[
subRect.height+'px',
'0px',
],
},{
duration: 300,
iterations: 1,
fill: 'forwards',
easing: 'linear',
});
this.animate({
opacity:[
1,
0,
],
},{
duration: 300,
iterations: 1,
fill: 'forwards',
easing: 'linear',
});
await sleep( 300 );
sub.remove();
//this.closed = true;
const closeRequest = new CustomEvent(
'closed',
{
bubbles: true,
composed: true,
//TODO closing details, button etc.
}
);
this.dispatchEvent( closeRequest );
//this.remove();
}
@property({ type: Number })
get timeout(){
return this.__timeout;
}
set timeout( newTimeout ){
const oldTimeout = this.__timeout;
this.__timeout = newTimeout;
this.requestUpdate('timeout', oldTimeout );
if( !newTimeout ) return;
this.updateComplete.then(() => {
this.__countDownStartTime = performance.now();
this.__countDownAnim = window.requestAnimationFrame( this.__countDown );
setTimeout(() => {
window.cancelAnimationFrame( this.__countDownAnim );
this.close();
}, newTimeout );
});
}
__countDown = ts => {
this.progress = Math.min(1, (ts - this.__countDownStartTime)/this.__timeout);
this.__countDownAnim = window.requestAnimationFrame( this.__countDown );
}
@property({ type: Number }) progress;
render(){
return html`
${ this.showCloseButton
? html`
${ this.progress ? html`
<mli-circular-progress
progress=${ 1 - this.progress }
class="close-button"
></mli-circular-progress>
`: null }
<mwc-icon-button
class="close-button"
icon="close"
@click=${ this._handlePrimaryAction }
></mwc-icon-button>
`: null
}
${ this.title ? html`
<div class="header">
<mli-icon icon=${ this.icon }></mli-icon>
${ this.title }
</div>
`: null }
<slot></slot>
<footer>
<slot
@click=${ this._handleSecondaryAction }
name="secondaryAction"></slot>
<slot
@click=${ this._handlePrimaryAction }
name="primaryAction"></slot>
</footer>
`;
}
}
import { LitElement, html, css, unsafeCSS, render } from 'lit';
import { state, property, customElement, queryAll } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
import { asyncAppend } from 'lit/directives/async-append.js';
import { pageStyles } from './mli-styles';
import './mli-toast';
import { MLIResume } from './task';
// TODO action callbacks
//
@customElement('mli-toaster')
export class MLIToaster extends LitElement {
static get styles() {
return [
pageStyles,
css`
:host {
gap: 3px;
align-items: flex-end;
}
::slotted(*),
mli-toast {
border-radius: 0.5em 0 0 0.5em;
}
`];
}
__sync = new MLIResume('toaster');
async* __getToasts() {
while( true ){
const toast = await this.__sync.reset();
yield toast;
}
}
toasts = this.__getToasts();
pop( toast ){
this.updateComplete.then(() => {
setTimeout(() => this.__sync.resolve( toast ));
});
}
toast( title, body, icon, col, bg, timeout = 0 ){
this.pop( html`
<mli-toast
style=${ styleMap({
"--mli-theme-primary": bg,
"--mli-theme-on-primary": col,
})}
timeout=${ timeout }
icon=${ icon }
title=${ title }
>${ body }</mli-toast>
`);
}
/*
connectedCallback(){
super.connectedCallback();
this.addEventListener('closed', this._handleItemClosed );
}
disconnectedCallback(){
this.removeEventListener('closed', this._handleItemClosed );
super.disconnectedCallback();
}
*/
render(){
// Slotted toasts will be kept at the bottom.
return html`
${ asyncAppend( this.toasts, t => t )}
<slot></slot>
`;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment