Some checks failed
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Failing after 52m26s
180 lines
No EOL
8.1 KiB
JavaScript
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); |