@alaskaairux/auro-interruption
Version:
Auro custom auro-interruption element
279 lines (240 loc) • 8.04 kB
JavaScript
/* eslint-disable no-underscore-dangle */
// Copyright (c) 2020 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
// See LICENSE in the project root for license information.
// ---------------------------------------------------------------------
import { LitElement, html } from "lit-element";
import { classMap } from 'lit-html/directives/class-map';
// Import touch detection lib
import "focus-visible/dist/focus-visible.min.js";
import 'wicg-inert';
import styleCss from "./style-css.js";
import styleCssFixed from './style-fixed-css.js';
import styleUnformattedCssFixed from './style-unformatted-fixed-css.js';
import closeIcon from '@alaskaairux/icons/dist/icons/interface/x-lg_es6.js';
/* eslint-disable one-var, prefer-destructuring */
const ESCAPE_KEYCODE = 27,
FOCUS_TIMEOUT = 50;
// See https://git.io/JJ6SJ for "How to document your components using JSDoc"
/**
* auro-dialog appear above the page and require the user's attention.
*
* @attr {Boolean} modal - Modal dialog restricts the user to take an action (no default close actions)
* @attr {Boolean} fixed - Uses fixed pixel values for element shape
* @attr {Boolean} unformatted - Unformatted dialog window, edge-to-edge fill for content
* @attr {Boolean} sm - Sets dialog box to small style. Adding both sm and lg will set the dialog to sm for desktop and lg for mobile.
* @attr {Boolean} md - Sets dialog box to medium style. Adding both md and lg will set the dialog to md for desktop and lg for mobile.
* @attr {Boolean} onDark - Sets close icon to white for dark backgrounds
* @attr {Boolean} open - Sets state of dialog to open
* @prop {HTMLElement} triggerElement - The element to focus when the dialog is closed. If not set, defaults to the value of document.activeElement when the dialog is opened.
* @slot header - Text to display as the header of the modal
* @slot content - Injects content into the body of the modal
* @slot footer - Used for action options, e.g. buttons
* @function toggleViewable - toggles the 'open' property on the element
* @event toggle - Event fires when the element is closed
* @csspart close-button - adjust position of the close X icon in the dialog window
* @csspart dialog-overlay - apply CSS on the overlay of the dialog
* @csspart dialog - apply CSS to the entire dialog
* @csspart dialog-header - apply CSS to the header of the dialog
* @csspart dialog-content - apply CSS to the content of the dialog
* @csspart dialog-footer - apply CSS to the footer of the dialog
*/
export default class ComponentBase extends LitElement {
constructor() {
super();
/**
* @private internal variable
*/
this.dom = new DOMParser().parseFromString(closeIcon.svg, 'text/html');
/**
* @private internal variable
*/
this.svg = this.dom.body.firstChild;
this.modal = false;
this.unformatted = false;
}
static get properties() {
return {
modal: { type: Boolean },
unformatted: {
type: Boolean,
reflect: true
},
open: {
type: Boolean,
reflect: true
},
triggerElement: {
attribute: false
}
};
}
firstUpdated() {
const slot = this.shadowRoot.querySelector("#footer"),
slotWrapper = this.shadowRoot.querySelector("#footerWrapper");
this.dialog = this.shadowRoot.getElementById('dialog');
if (!this.unformatted && slot.assignedNodes().length === 0) {
slotWrapper.classList.remove("dialog-footer");
}
}
/**
* LitElement lifecycle method. Called after the DOM has been updated.
* @param {Map<string, any>} changedProperties - keys are the names of changed properties, values are the corresponding previous values.
* @returns {void}
*/
updated(changedProperties) {
if (changedProperties.has('open')) {
if (this.open) {
this.openDialog();
} else {
this.closeDialog();
}
}
}
connectedCallback() {
super.connectedCallback();
this.keydownEventHandler = this.handleKeydown.bind(this);
window.addEventListener('keydown', this.keydownEventHandler);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('keydown', this.keydownEventHandler);
}
/**
* @private
* @returns {void}
*/
openDialog() {
this.defaultTrigger = document.activeElement;
setTimeout(() => {
this.focus();
this.handleFocusLoss();
}, FOCUS_TIMEOUT);
}
/**
* @private
* @returns {void}
*/
closeDialog() {
this.dispatchToggleEvent();
}
/**
* @private
* @returns {void}
*/
dispatchToggleEvent() {
// replace with Event constructor once IE support dropped
const toggleEvent = document.createEvent("HTMLEvents");
toggleEvent.initEvent("toggle", true, false);
this.dispatchEvent(toggleEvent);
}
/**
* @private
* @returns {void} Determines if dropdown bib should be closed on focus change.
*/
handleFocusLoss() {
const focusable = [...this.querySelectorAll('button, auro-button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')];
const firstFocusableElement = focusable[0];
const lastFocusableElement = focusable[focusable.length - 1];
const closeButton = this.shadowRoot.getElementById('dialog-close');
lastFocusableElement.addEventListener('focusout', () => {
if (closeButton !== null) { // eslint-disable-line no-negated-condition
closeButton.focus();
} else {
firstFocusableElement.focus();
}
});
}
/**
* @private
* @returns {void}
*/
handleOverlayClick() {
if (this.open && !this.modal) {
this.handleCloseButtonClick();
}
}
/**
* @private
* @returns {void}
*/
handleCloseButtonClick() {
this.open = false;
}
/**
* @private
* @returns {void}
*/
handleKeydown({ key, keyCode }) {
if (this.open && !this.modal && (key === 'Escape' || keyCode === ESCAPE_KEYCODE)) {
this.open = false;
}
}
/**
* @private
* Focus the dialog.
* @returns {void}
*/
focus() {
if (this.open) {
this.dialog.focus();
}
}
static get styles() {
return [
styleCss,
styleCssFixed,
styleUnformattedCssFixed
];
}
/**
* @private
* @returns {TemplateResult} the close button template
*/
getCloseButton() {
return this.modal
? html``
: html`
<button class="dialog-header--action" id="dialog-close" ="${this.handleCloseButtonClick}" part="close-button">
<span>${this.svg}</span>
<span class="util_displayHiddenVisually">Close</span>
</button>
`
}
render() {
const classes = {
'dialogOverlay': true,
'dialogOverlay--modal': this.modal && this.open,
'dialogOverlay--open': this.open,
'util_displayHidden': !this.open
},
contentClasses = {
'dialog': true,
'dialog--open': this.open
}
return html`
<div class="${classMap(classes)}" id="dialog-overlay" part="dialog-overlay" =${this.handleOverlayClick}></div>
<div role="dialog" id="dialog" class="${classMap(contentClasses)}" part="dialog" aria-labelledby="dialog-header" tabindex="-1">
${this.unformatted
? html`
<slot name="content"></slot>
${this.getCloseButton()}
`
: html`
<div class="dialog-header" part="dialog-header">
<h1 class="heading heading--700 util_stackMarginNone--top" id="dialog-header">
<slot name="header">Default header ...</slot>
</h1>
${this.getCloseButton()}
</div>
<div class="dialog-content" part="dialog-content">
<slot name="content"></slot>
</div>
<div class="dialog-footer" id="footerWrapper" part="dialog-footer">
<slot name="footer" id="footer"></slot>
</div>
`
}
</div>
`;
}
}