Skip to content

Instantly share code, notes, and snippets.

@megasmack
Last active October 9, 2024 13:24
Show Gist options
  • Save megasmack/8ccc915203766058097d8f642f192b99 to your computer and use it in GitHub Desktop.
Save megasmack/8ccc915203766058097d8f642f192b99 to your computer and use it in GitHub Desktop.
LWC Focus-Trapping Modal

LWC Focus-Trapping Modal

Using SLDS classes, build a modal Lightning Web Component that has focus-trapping for accessiblity.

Notes

  • The shadowdom presents some challenges with creating focus-trapping within modals. Making creating a reusable component tricky. So instead we can create a modal utility file with all our methods to keep things as DRY as possible.
  • This was created before the Lightning Modal component was created. The "out of the box" Lightning Modal contains focus trapping, so I'd recommend using that over this implmentation. However, you can still use code for "modal-like" custom elements that may require focus trapping. For example, a slide out navigation that covers the page content could be considered a modal and should therefore have focus-trapping when open.
<template>
<div role="alertdialog"
tabindex="-1"
aria-labelledby="modal-heading"
aria-modal="true"
aria-describedby="modal-content"
class={modalClasses}>
<div class="slds-modal__container">
<header class="slds-modal__header">
<lightning-button-icon icon-name="utility:close"
title={closeLabel}
size="large"
variant="bare"
class="slds-modal__close slds-button_icon-inverse slds-medium-size"
onclick={handleModalClose}>
</lightning-button-icon>
<div id="modal-heading">
{headerTitle}
</div>
</header>
<div if:true={isModalOpen}
class="slds-modal__content slds-p-around_large"
id="modal-content">
<lightning-formatted-rich-text value={formattedMessage}>
</lightning-formatted-rich-text>
</div>
<footer class="slds-modal__footer">
<button class="slds-button slds-button_inverse"
onclick={handleModalClose}>
{closeLabel}
</button>
<button class="slds-button slds-button_brand"
onclick={handleModalAccept}>
{acceptLabel}
</button>
</footer>
</div>
</div>
<div class={modalBgClasses}></div>
</template>
import { LightningElement, api } from 'lwc';
import {
getModalClasses,
getModalBgClasses,
modalWindowKeyUp,
modalWindowKeyDown,
modalKeyDown,
modalCloseFocus
} from 'c/modalUtils';
// Replace these with Custom Label
const CloseLabel = 'Close';
const AcceptLabel = 'Accept';
const HeaderTitle = 'Modal Header';
const FormattedMessage = 'Some modal body content.';
export default class X7sAlertDialog extends LightningElement {
isModalOpen = false;
handleWindowKeyDownEvent;
handleWindowKeyUpEvent;
handleKeyDownEvent;
headerTitle = HeaderTitle;
formattedMessage = FormattedMessage;
closeLabel = CloseLabel;
acceptLabel = AcceptLabel;
connectedCallback() {
this.handleWindowKeyDownEvent = this.handleWindowKeyDown.bind(this);
this.handleWindowKeyUpEvent = this.handleWindowKeyUp.bind(this);
this.handleKeyDownEvent = this.handleKeyDown.bind(this);
window.addEventListener('keydown', this.handleWindowKeyDownEvent);
window.addEventListener('keyup', this.handleWindowKeyUpEvent);
this.addEventListener('keydown', this.handleKeyDownEvent);
}
renderedCallback() {
if (this.isModalOpen) {
modalCloseFocus(this.template);
}
}
disconnectedCallback() {
window.removeEventListener('keydown', this.handleWindowKeyDownEvent);
window.removeEventListener('keyup', this.handleWindowKeyUpEvent);
this.removeEventListener('keydown', this.handleKeyDownEvent);
}
get modalClasses() {
return getModalClasses(this.isModalOpen, 'medium');
}
get modalBgClasses() {
return getModalBgClasses(this.isModalOpen);
}
handleModalOpen() {
this.isModalOpen = true;
}
handleModalClose() {
this.isModalOpen = false;
}
handleWindowKeyUp(event) {
modalWindowKeyUp(event);
}
handleWindowKeyDown(event) {
modalWindowKeyDown(event);
}
handleKeyDown(event) {
modalKeyDown(event, this.template);
}
handleModalAccept() {
// Insert Accept action
this.handleModalClose();
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>54.0</apiVersion>
<isExposed>false</isExposed>
<masterLabel>Alert Dialog</masterLabel>
</LightningComponentBundle>
let isShiftKeyDown = false;
/**
* Toggle show/hide modal classes
* @param {boolean} showModal
* @param {undefined|'small'|'medium'|'large'}size
* @returns {string}
*/
export const getModalClasses = (showModal, size = undefined) => {
let classes = 'slds-modal';
classes += showModal ? ' slds-fade-in-open' : '';
classes += size === 'small' ? ' slds-modal_small' : '';
classes += size === 'medium' ? ' slds-modal_medium' : '';
classes += size === 'large' ? ' slds-modal_large' : '';
return classes;
};
/**
* Toggle show/hide backdrop modal classes
* @param showModal
* @returns {string}
*/
export const getModalBgClasses = (showModal) => {
const defaultClass = 'slds-backdrop';
return showModal ? `${defaultClass} slds-backdrop_open` : defaultClass;
};
export const modalWindowKeyUp = (event) => {
// Shift
if (event.key === 'Shift') {
isShiftKeyDown = false;
}
};
export const modalWindowKeyDown = (event) => {
// Shift
if (event.key === 'Shift') {
isShiftKeyDown = true;
}
};
export const modalKeyDown = (event, template) => {
/** @type {HTMLElement} */
const activeEl = template.activeElement;
/** @type {HTMLElement} */
const closeEl = template.querySelector('.slds-modal__close');
/** @type {HTMLElement[]} */
const focusEls = template.querySelectorAll(
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
);
/** @type {HTMLElement} */
const lastEl =
focusEls?.length > 0 ? focusEls[focusEls.length - 1] : closeEl;
// Escape
if (event.key === 'Escape') {
this.close();
}
// Tab
if (event.key === 'Tab' && activeEl) {
if (!isShiftKeyDown && activeEl === lastEl) {
event.preventDefault();
closeEl.focus();
} else if (isShiftKeyDown && activeEl === closeEl) {
event.preventDefault();
lastEl.focus();
}
}
};
/**
* Focus the Close Button
* @param template
*/
export const modalCloseFocus = (template) => {
/** @type {HTMLElement} */
const closeEl = template.querySelector('.slds-modal__close');
if (closeEl) {
closeEl.focus();
}
};
<?xml version="1.0" encoding="UTF-8" ?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>54.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
@Jayant-tembhurkar
Copy link

bhai iski html file?

@megasmack
Copy link
Author

bhai iski html file?

This is a utility file to be referenced by other components. It has no HTML file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment