@freshworks/crayons
Version:
Crayons Web Components library
674 lines (673 loc) • 19.4 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Element, Event, Method, Prop, State, Watch, h, } from '@stencil/core';
import { getFocusableChildren } from '../../utils';
import { i18n } from '../../global/Translation';
export class Modal {
constructor() {
/**
* Property to add or remove the top right close icon button
*/
this.hasCloseIconButton = true;
/**
* The icon to be displayed with the title
*/
this.icon = '';
/**
* Size of the modal
*/
this.size = 'standard';
/**
* The text for the submit button
*/
this.submitText = '';
/**
* The text for the cancel button
*/
this.cancelText = '';
/**
* Default state of submit button
*/
this.submitDisabled = false;
/**
* The color of submit button
*/
this.submitColor = 'primary';
/**
* Hide footer for the modal
*/
this.hideFooter = false;
/**
* Convert modal to slider
*/
this.slider = false;
/**
* Toggle the visibility of the modal
*/
this.isOpen = false;
/**
* private
* Modal container ref
*/
this.modalContainer = null;
/**
* private
* Handler to run on modal container opening
*/
this.modalContainerHandler = null;
/**
* private
* Modal first focus element
*/
this.firstFocusableElement = null;
/**
* private
* Handler to first focusable element. Focuses last element on tab for focus locking.
*/
this.firstFocusableElementHandler = null;
/**
* private
* Modal last focus element
*/
this.lastFocusableElement = null;
/**
* private
* Handler for last focusable element. Focus first element on shift+tab for focus locking.
*/
this.lastFocusableElementHandler = null;
/**
* private
* Modal element to focus on open
*/
this.modalOpenFocusElement = null;
/**
* private
* flag to add events to elements only on initial modal load
*/
this.accessibilityAdded = false;
/**
* private
* escape key handler
*/
this.escapeHandler = null;
/**
* private
* styleVariation styles that vary in normal and slider variations
*/
this.styleVariation = {
closeColor: {
modal: '#475867',
slider: '#FFFFFF',
},
closeSize: {
modal: 10,
slider: 12,
},
closeName: {
modal: 'cross',
slider: 'cross-big',
},
};
}
/**
* lifecycle event, called once just after the component is first connected to the DOM
*/
componentWillLoad() {
if (!this.modalTitle) {
this.modalTitle = this.el.querySelector('fw-modal-title');
}
if (!this.modalFooter) {
this.modalFooter = this.el.querySelector('fw-modal-footer');
if (this.modalFooter) {
this.modalFooter.submit = this.submit.bind(this);
this.modalFooter.close = this.close.bind(this);
}
}
if (!this.modalContent) {
this.modalContent = this.el.querySelector('fw-modal-content');
}
if (this.hideFooter && this.modalFooter) {
this.modalFooter.style.display = 'none';
}
if (!this.modalContent && (this.modalTitle || this.modalFooter)) {
/**
* Error that occurs when fw-modal-header or fw-modal-footer is used without
* fw-modal-content component. If this not handled, the content that goes inside
* fw-modal-content would have fw-modal-header or fw-modal-footer.
* This would lead to unexpected results such as header or footer having padding and scroll.
*/
throw new Error('Composition Error: fw-modal component must have fw-modal-content component when \
either fw-modal-header or fw-modal-footer components are used for modal composition');
}
}
componentDidLoad() {
if (this.isOpen && !this.accessibilityAdded) {
document.body.style.overflow = 'hidden';
this.addAccesibilityEvents();
}
}
disconnectedCallback() {
if (this.isOpen) {
document.body.style.overflow = 'auto';
this.removeAccesibilityEvents();
}
}
visibilityChange(open) {
if (!open) {
document.body.style.overflow = 'auto';
this.removeAccesibilityEvents();
this.fwClose.emit();
}
else {
document.body.style.overflow = 'hidden';
this.addAccesibilityEvents();
this.fwOpen.emit();
}
}
footerVisibilityChange(hideFooter) {
if (this.modalFooter) {
if (hideFooter) {
this.modalFooter.style.display = 'none';
}
else {
this.modalFooter.style.display = 'block';
}
}
}
/**
* Method available from the component to perform close action on the modal
* @returns promise that resolves to true
*/
async close() {
this.isOpen = false;
return true;
}
/**
* Method available from the component to perform open action on the modal
* @returns promise that resolves to true
*/
async open() {
this.isOpen = true;
return true;
}
/**
* private submit
*/
submit() {
this.fwSubmit.emit();
}
/**
* Adds accesibility related events to the component.
* Major actions would be to focus-lock inside modal and to focus on close button when opening the component.
*/
addAccesibilityEvents() {
var _a, _b, _c;
if (!this.accessibilityAdded ||
!((_a = this.firstFocusableElement) === null || _a === void 0 ? void 0 : _a.parentNode) ||
!((_b = this.modalOpenFocusElement) === null || _b === void 0 ? void 0 : _b.parentNode) ||
!((_c = this.lastFocusableElement) === null || _c === void 0 ? void 0 : _c.parentNode)) {
this.modalContainerHandler &&
this.modalContainer.removeEventListener('animationend', this.modalContainerHandler);
this.modalContainerHandler = (() => {
this.firstFocusableElementHandler &&
this.firstFocusableElement.removeEventListener('keydown', this.firstFocusableElementHandler);
this.lastFocusableElementHandler &&
this.lastFocusableElement.removeEventListener('keydown', this.lastFocusableElementHandler);
/**
* Focus trapping inside Modal. Below function gets all focusable elements from the modal.
* These include elements inside shadow dom too.
*/
const focusableElements = getFocusableChildren(this.el);
if (focusableElements.length) {
this.firstFocusableElement = focusableElements[0];
this.lastFocusableElement =
focusableElements[focusableElements.length - 1];
this.modalOpenFocusElement =
focusableElements.length > 1 &&
this.firstFocusableElement.classList.contains('close-btn')
? focusableElements[1]
: this.firstFocusableElement;
this.lastFocusableElementHandler = ((e) => {
if (!e.shiftKey && e.keyCode === 9) {
e.preventDefault();
this.focusElement(this.firstFocusableElement);
}
}).bind(this);
this.firstFocusableElementHandler = ((e) => {
if (e.shiftKey && e.keyCode === 9) {
e.preventDefault();
this.focusElement(this.lastFocusableElement);
}
}).bind(this);
this.lastFocusableElement.addEventListener('keydown', this.lastFocusableElementHandler);
this.firstFocusableElement.addEventListener('keydown', this.firstFocusableElementHandler);
}
if (this.isOpen && this.modalOpenFocusElement) {
this.focusElement(this.modalOpenFocusElement);
}
}).bind(this);
this.modalContainer &&
this.modalContainer.addEventListener('animationend', this.modalContainerHandler);
this.accessibilityAdded = true;
}
this.escapeHandler = ((e) => {
if (e.keyCode === 27) {
this.isOpen = false;
}
}).bind(this);
document.addEventListener('keydown', this.escapeHandler);
}
/**
* Removes accesibility related events bound to the document.
*/
removeAccesibilityEvents() {
if (this.escapeHandler) {
document.removeEventListener('keydown', this.escapeHandler);
this.escapeHandler = null;
}
}
/**
* @param element element to focus on
*/
focusElement(element) {
try {
if (element.setFocus) {
element.setFocus();
}
else {
element.focus();
}
}
catch (error) {
console.log(error);
}
}
/**
* Renders Icon in Modal header.
* @returns {JSX.Element}
*/
renderIcon() {
return h("fw-icon", { class: 'icon', name: this.icon, size: 16 });
}
/**
* Renders title text and description in modal header.
* @returns {JSX.Element}
*/
renderTitle() {
return (h("fw-modal-title", { icon: this.icon, titleText: this.titleText, description: this.description }));
}
/**
* renders the slot content in the modal.
* @returns {JSX.Element}
*/
renderContent() {
return (h("fw-modal-content", null,
h("slot", null)));
}
/**
* renders the default footer in the modal
* @returns {JSX.Element}
*/
renderFooter() {
return (h("fw-modal-footer", { submitText: this.submitText, cancelText: this.cancelText, submitDisabled: this.submitDisabled, submitColor: this.submitColor, submit: this.submit.bind(this), close: this.close.bind(this), style: { display: this.hideFooter ? 'none' : 'block' } }));
}
/**
* renders either default modal content based on attributes or renders the modal child components like
* modal-title, modal-content and modal-footer components.
* @returns {JSX.Element}
*/
render() {
const variation = this.styleVariation;
return (h("div", { class: {
'modal-overlay': true,
'visible': this.isOpen,
'slider': this.slider,
} },
h("div", { class: { modal: true, [this.size]: true }, "aria-modal": 'true', ref: (el) => (this.modalContainer = el) },
this.hasCloseIconButton && (h("button", { class: 'close-btn', onClick: () => this.close() },
h("fw-icon", { name: this.slider
? variation.closeName.slider
: variation.closeName.modal, library: 'system', color: this.slider
? variation.closeColor.slider
: variation.closeColor.modal, size: this.slider
? variation.closeSize.slider
: variation.closeSize.modal }))),
h("div", { class: 'modal-container' },
this.modalTitle ? '' : this.titleText ? this.renderTitle() : '',
this.modalContent ? h("slot", null) : this.renderContent(),
this.modalFooter ? '' : this.renderFooter()))));
}
static get is() { return "fw-modal"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() { return {
"$": ["modal.scss"]
}; }
static get styleUrls() { return {
"$": ["modal.css"]
}; }
static get properties() { return {
"hasCloseIconButton": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Property to add or remove the top right close icon button"
},
"attribute": "has-close-icon-button",
"reflect": false,
"defaultValue": "true"
},
"titleText": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The title text to be displayed on the modal"
},
"attribute": "title-text",
"reflect": false
},
"description": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The description text to be displayed on the modal"
},
"attribute": "description",
"reflect": false
},
"icon": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The icon to be displayed with the title"
},
"attribute": "icon",
"reflect": false,
"defaultValue": "''"
},
"size": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'standard' | 'small' | 'large'",
"resolved": "\"large\" | \"small\" | \"standard\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Size of the modal"
},
"attribute": "size",
"reflect": false,
"defaultValue": "'standard'"
},
"submitText": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The text for the submit button"
},
"attribute": "submit-text",
"reflect": false,
"defaultValue": "''"
},
"cancelText": {
"type": "string",
"mutable": true,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The text for the cancel button"
},
"attribute": "cancel-text",
"reflect": false,
"defaultValue": "''"
},
"submitDisabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Default state of submit button"
},
"attribute": "submit-disabled",
"reflect": false,
"defaultValue": "false"
},
"submitColor": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'primary' | 'secondary' | 'danger' | 'link' | 'text'",
"resolved": "\"danger\" | \"link\" | \"primary\" | \"secondary\" | \"text\"",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "The color of submit button"
},
"attribute": "submit-color",
"reflect": false,
"defaultValue": "'primary'"
},
"hideFooter": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Hide footer for the modal"
},
"attribute": "hide-footer",
"reflect": false,
"defaultValue": "false"
},
"slider": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Convert modal to slider"
},
"attribute": "slider",
"reflect": false,
"defaultValue": "false"
},
"isOpen": {
"type": "boolean",
"mutable": true,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Toggle the visibility of the modal"
},
"attribute": "is-open",
"reflect": true,
"defaultValue": "false"
}
}; }
static get states() { return {
"modalTitle": {},
"modalFooter": {},
"modalContent": {}
}; }
static get events() { return [{
"method": "fwSubmit",
"name": "fwSubmit",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Triggered when the default action button is clicked."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "fwOpen",
"name": "fwOpen",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Triggered when modal is opened."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "fwClose",
"name": "fwClose",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Triggered when modal is closed."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}]; }
static get methods() { return {
"close": {
"complexType": {
"signature": "() => Promise<boolean>",
"parameters": [],
"references": {
"Promise": {
"location": "global"
}
},
"return": "Promise<boolean>"
},
"docs": {
"text": "Method available from the component to perform close action on the modal",
"tags": [{
"name": "returns",
"text": "promise that resolves to true"
}]
}
},
"open": {
"complexType": {
"signature": "() => Promise<boolean>",
"parameters": [],
"references": {
"Promise": {
"location": "global"
}
},
"return": "Promise<boolean>"
},
"docs": {
"text": "Method available from the component to perform open action on the modal",
"tags": [{
"name": "returns",
"text": "promise that resolves to true"
}]
}
}
}; }
static get elementRef() { return "el"; }
static get watchers() { return [{
"propName": "isOpen",
"methodName": "visibilityChange"
}, {
"propName": "hideFooter",
"methodName": "footerVisibilityChange"
}]; }
}
__decorate([
i18n({ keyName: 'modal.ok' })
], Modal.prototype, "submitText", void 0);
__decorate([
i18n({ keyName: 'modal.cancel' })
], Modal.prototype, "cancelText", void 0);