/** * 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);