confirmation-animated-dialog
Version:
A simple, ARIA-standarized, responsive yes/no dialog window.
250 lines (240 loc) • 7.21 kB
JavaScript
import { LitElement, html, css } from 'https://unpkg.com/@polymer/lit-element@latest/lit-element.js?module';
export class ConfirmationAnimatedDialog extends LitElement{
static get properies(){
return {
open: {
type: Boolean,
attribute: "open",
reflect: true
},
headline: {
type: String,
attribute: "headline",
reflect: true
},
content: {
type: String,
attribute: "content",
reflect: true
},
buttons:{
type: Boolean,
attribute: "buttons",
reflect: true
},
yes: {
type: String,
attribute: "yes",
reflect: true
},
no: {
type: String,
attribute: "no",
reflect: true
}
}
};
static get styles(){
return[css `
:host{
width: auto;
height: auto;
margin: 0;
padding: 0;
z-index:999;
color: var(--general-text-color, black);
}
#header, #content, #buttons {
opacity: 0;
animation: bringUp .4s forwards;
}
#back {
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
background-color: var(--background-color, rgba(0,0,0, 0.5));
display: flex;
justify-content: center;
align-items: center;
overflow: scroll;
}
#modal {
background-color: var(--dialog-background-color, white);
width: 20rem;
height: max-content;
border-radius: 2%;
box-shadow: 1rem .4rem 3rem rgba(0,0,0,0.6);
padding: var(--general-padding, 2rem);
display: flex;
flex-direction: column;
font-family: sans-serif;
transform: scale(.2);
animation: comeUp .5s forwards;
margin-top: -8rem;
}
comeUp {
to{
transform: scale(1);
}
}
#content{
flex-grow: 1;
margin-bottom: var(--general-padding, 2rem);
animation-delay: .5s;
}
#buttons{
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
align-content: space-between;
align-items: space-between;
animation-delay: .8s;
}
#header {
text-align: center;
animation-delay: .2s;
}
button{
background-color: white;
border: none;
box-shadow: none;
padding: .5rem;
width: 40%;
background-color: var(--yes-button-background-color, #50DDF0);
font-size: 120%;
color: var(--yes-button-text-color, white);
}
button:focus {
outline: 1px solid gray;
}
#no {
background-color: var(--no-button-background-color, #E2E5FE);
color: var(--no-button-text-color, black);
}
#close {
position: absolute;
top: var(--close-button-top, 1.5rem);
right: var(--close-button-right, 1rem);
color: var(--close-button-text-color, rgba(0,0,0, 0.4));
cursor: pointer;
border: none;
width: max-content;
background-color: var(--dialog-background-color, white);
}
#close:hover{
color: var(--close-button-text-color-hover, black);
}
#close:focus{
outline: 1px solid blue;
}
bringUp{
to {
opacity: 1;
}
}
only screen and (max-width: 400px){
#modal{
width: 14rem;
}
}
`
]
};
constructor(){
super();
this.open = true;
this.prevFocus = null;
this.buttons = true;
this.yes = "Yes";
this.no = "No";
}
firstUpdated(){
super.firstUpdated();
this.prevFocus = document.activeElement;
if(this.buttons){
this.shadowRoot.querySelector("#no").focus();}
else{
this.shadowRoot.querySelector("#close").focus();
}
let buttons = this.shadowRoot.querySelector("#buttons");
buttons.addEventListener("focusin", this.trap.bind(this));
buttons.addEventListener("focusout", this.untrap.bind(this));
}
hide(e){
e.preventDefault();
let response;
switch(e.target.textContent){
case "x":
response = false;
break;
case this.yes:
response = true;
break;
case this.no:
response = false;
}
this.dispatchEvent(new CustomEvent('answer', { bubbles: true, detail: { text: () => response, focused: () => this.prevFocus} }));
this.open = false;
this.style.display = "none";
}
connectedCallback(){
super.connectedCallback();
if(this.open){
this.style.display = "block";
}
else{
this.style.display = "none";
}
}
jumpToTheEnd(){
this.shadowRoot.querySelector("#yes").focus();
}
jumpToStart(){
this.shadowRoot.querySelector("#close").focus();
}
trap(){
this.shadowRoot.querySelector("#start").tabIndex = 0;
this.shadowRoot.querySelector("#stop").tabIndex = 0;
}
untrap(){
this.shadowRoot.querySelector("#start").tabIndex = -1;
this.shadowRoot.querySelector("#stop").tabIndex = -1;
}
render(){
return html`
<div id="back">
<div id="modal" role="dialog" aria-labelledby="header" aria-describedby="content">
<h1 id="header">${this.headline}</h1>
<p id="content">${this.content}</p>
<div id="start" ="${this.jumpToTheEnd}" tabindex="-1"></div>
<div id="buttons">
<button id="close" ="${this.hide}">x</button>
${this.buttons ? html `
<button id="no" ="${this.hide}">${this.no}</button>
<button id="yes" ="${this.hide}">${this.yes}</button>
</div>` :html ` <button style="width: 100%" id="yes" ="${this.hide}">${this.yes}</button>
</div>`}
<div id="stop" ="${this.jumpToStart}" tabindex="-1"></div>
</div>
</div></div>
`
}
}
customElements.define("confirmation-animated-dialog", ConfirmationAnimatedDialog);
export async function showModal(headline, content, buttons=true, yes="Yes", no="No") {
const $dialog = await new ConfirmationAnimatedDialog();
document.body.appendChild($dialog);
$dialog.open = true;
$dialog.headline = headline;
$dialog.content = content;
$dialog.buttons = buttons;
$dialog.yes = yes;
$dialog.no = no;
window.addEventListener("answer", e =>{ e.detail.focused().focus();})
return $dialog;
}