ba-web-components/src/components/drawer/drawer.js
Alan Bridgeman 5024375e20
Some checks failed
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Failing after 52m26s
Inital code commit
2026-05-13 01:39:35 -05:00

180 lines
No EOL
8.1 KiB
JavaScript

/**
* Drawer Web Component
*
* A simple drawer component that toggles the visibility of its content when the header is clicked or activated with the keyboard.
*/
class Drawer extends HTMLElement {
constructor() {
super();
// Event handler bindings for click and keydown events on the drawer handle (button)
this._handleClick = this._clicked.bind(this);
this._handleKeyDown = this._keyDown.bind(this)
}
// ----------------------
// Private Event Handlers
// ----------------------
/** Click handler for the drawer "handle" (button) */
_clicked(event) {
event.preventDefault();
event.currentTarget.setAttribute('aria-expanded', event.currentTarget.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
this.contentDiv.classList.toggle('hidden');
}
/** Keydown handler for the drawer "handle" (button) */
_keyDown(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.currentTarget.setAttribute('aria-expanded', event.currentTarget.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
this.contentDiv.classList.toggle('hidden');
}
}
// -------------------------------
// Web Component Lifecycle Methods
// -------------------------------
/**
* Does initial setup and adds event listeners for interactivity
*
* `connectedCallback` is a lifecycle method in web components that runs when the custom element is inserted into the document's Document Object Model (DOM).
* It can be invoked multiple times if the element is removed and then re-inserted into the DOM.
*
* Timing: It is called after the element's constructor() but before the element's children are necessarily connected or fully rendered.
* Purpose: It is the ideal place to set up tasks that should only occur when the element is actually present in the live document. Common uses include:
*/
connectedCallback() {
const internals = this.attachInternals();
this.shadow = this.shadowRoot;
if (!this.shadow) {
this.shadow = this.attachShadow({ mode: 'open' });
// Defer execution until the browser finishes parsing the children
setTimeout(() => {
// Recreate the template using the shadow DOM that is only available through JavaScript
this.createTemplateInJS(this.shadow);
}, 0);
}
setTimeout(() => {
// Native Popover handles the click, enter, space, and dismiss logic.
// You only need event listeners here if you want to trigger
// custom analytics or highly specific behavior on open/close.
const drawerHandle = this.shadow.querySelector('[aria-controls]');
if (drawerHandle) {
// Get the content div that the drawer handle controls so we can toggle it in the click and keydown handlers
this.contentDiv = this.shadow.querySelector(`#${drawerHandle.getAttribute('aria-controls')}`);
// Add event listeners to the drawer handle (button)
drawerHandle.addEventListener('click', this._handleClick);
drawerHandle.addEventListener('keydown', this._handleKeyDown);
}
}, 0);
}
/**
* Cleans up event listeners when the component is removed from the DOM
*
* `disconnectedCallback` is a lifecycle method in web components that runs when the custom element is removed from the document's DOM.
* It can be invoked multiple times if the element is removed and then re-inserted into the DOM.
*
* Timing: It is called after the element is removed from the DOM but before it is garbage collected.
* Purpose: It is the ideal place to clean up any resources or event listeners that were set up in `connectedCallback`.
*
* Common uses include:
* - Removing event listeners to prevent memory leaks
* - Clearing timers or intervals
* - Disconnecting from external data sources or APIs
*/
disconnectedCallback() {
const drawerHandle = this.shadow.querySelector('[aria-controls]');
if (drawerHandle) {
drawerHandle.removeEventListener('click', this._handleClick);
drawerHandle.removeEventListener('keydown', this._handleKeyDown);
}
}
/**
* Recreate the template in the shadow DOM through JavaScript instead of relying on the `shadowrootmode` attribute
*
* @param {ShadowRoot} shadow The shadow DOM to attach the template to
*/
createTemplateInJS(shadow) {
// Retrieve the scoped data island
const configScript = this.querySelector('script[data-drawer-config]');
if (!configScript) {
console.error('Drawer configuration missing. Cannot build JS fallback.');
return;
}
const config = JSON.parse(configScript.textContent);
const { drawerExtraStyles, drawerExtraScripts, drawerId, componentsStyleHref } = config;
// Create and append the link tag for the components library CSS (including component specific styles)
const drawerStyle = document.createElement('link');
drawerStyle.rel = 'stylesheet';
drawerStyle.type = 'text/css';
drawerStyle.href = componentsStyleHref;
shadow.appendChild(drawerStyle);
// Create additional link tags for any extra CSS files to include
drawerExtraStyles.forEach(style => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = `/css/${style}.css`;
shadow.appendChild(link);
});
// Create and append script tags for any extra JavaScript files to include
drawerExtraScripts.forEach(script => {
const scriptElem = document.createElement('script');
scriptElem.type = typeof script === 'object' && typeof script.module === 'boolean' && script.module ? 'module' : typeof script === 'string' && script.endsWith('.mjs') ? 'module' : 'application/javascript';
scriptElem.src = typeof script === 'object' ? script.script.startsWith('http') ? script.script : `/js/${script.script.endsWith('.mjs') ? script.script : script.script + '.js'}` : script.startsWith('http') ? script : `/js/${script.endsWith('.mjs') ? script : script + '.js'}`;
shadow.appendChild(scriptElem);
});
// Create the container div for the drawer component
const drawerContainerDiv = document.createElement('div');
drawerContainerDiv.id = drawerId;
drawerContainerDiv.classList.add('drawer');
// Create the button element for the tooltip trigger
const drawerBtnElem = document.createElement('div');
drawerBtnElem.id = `${drawerId}-drawer-header`;
drawerBtnElem.role = 'button';
drawerBtnElem.classList.add('drawer-header');
drawerBtnElem.tabIndex = 0;
drawerBtnElem.setAttribute('aria-expanded', 'false');
drawerBtnElem.setAttribute('aria-controls', `${drawerId}-drawer-contents`);
const drawerHeaderSlotElem = document.createElement('slot');
drawerHeaderSlotElem.name = 'drawer-header';
drawerBtnElem.appendChild(drawerHeaderSlotElem);
drawerContainerDiv.appendChild(drawerBtnElem);
// Create the div that will contain the drawer content
const contentDiv = document.createElement('div');
contentDiv.id = `${drawerId}-drawer-contents`;
contentDiv.classList.add('drawer-contents', 'hidden');
const contentSlotElem = document.createElement('slot');
contentSlotElem.name = 'drawer-contents';
contentDiv.appendChild(contentSlotElem);
drawerContainerDiv.appendChild(contentDiv);
// Finally, append the entire drawer container div to the shadow DOM of the component
shadow.appendChild(drawerContainerDiv);
}
}
customElements.define('ba-drawer', Drawer);