Inital code commit
Some checks failed
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Failing after 52m26s

This commit is contained in:
Alan Bridgeman 2026-05-13 01:39:35 -05:00
commit 5024375e20
32 changed files with 5379 additions and 0 deletions

View file

@ -0,0 +1,19 @@
# Accessible Drawer
Drawers allow for the opening and closing of information sections.
## Usage
to use this component:
```ejs
<%- useComponent('drawer', { id: 'example-drawer', content: `<div><h2>Hello World!</h2><p>This is a drawer!</p></div>` }) %>
```
## Parameters
| Parameter | type | Description |
| -------------------- | ------- | ------------------------------------------------------------ |
| `id` | string | The ID of the drawer |
| `label` | HTML | The label on the drawer's "handle" (button) |
| `content` | HTML | The content of the drawer |
| `drawerExtraStyles` | Various | Extra CSS to be included in the components Shadow DOM |
| `drawerExtraScripts` | Various | Extra JS scripts to be included in the components Shadow DOM |

View file

@ -0,0 +1,25 @@
.drawer {
display: flex;
width: 100%;
flex-direction: column;
}
.drawer-header {
padding: 0.5rem 1rem;
cursor: pointer;
border: 1px solid light-dark(#000000, #FFFFFF);
}
.drawer-header:hover, .drawer-header:focus {
background-color: light-dark(#000000, #FFFFFF);
color: light-dark(#FFFFFF, #000000);
}
.drawer-contents {
padding: 0.5rem 1rem;
}
.hidden {
display: none;
visibility: hidden;
}

View file

@ -0,0 +1,62 @@
<ba-drawer>
<%# Because of complexities with web components and specifically more legacy support and the `createTemplateInJS` method, we need to pass the drawer data available in EJS to the JavaScript for the web component (easiest way was via a script tag) %>
<!-- JSON config data so that `createTemplateInJS` can recreate the drawer DOM structure accurately if needed -->
<script type="application/json" data-drawer-config>
<%- JSON.stringify({
drawerId: id,
drawerExtraStyles: typeof drawerExtraStyles !== 'undefined' ? (Array.isArray(drawerExtraStyles) ? drawerExtraStyles : [drawerExtraStyles]) : [],
drawerExtraScripts: typeof drawerExtraScripts !== 'undefined' ? (Array.isArray(drawerExtraScripts) ? drawerExtraScripts : [drawerExtraScripts]) : [],
componentsStyleHref
}) %>
</script>
<template shadowrootmode="open">
<!-- Styles -->
<!-- Component Specific Styling -->
<!-- Component Styling (Part of Component Library CSS) -->
<link rel="stylesheet" type="text/css" href="<%= componentsStyleHref %>">
<!-- END: Component Specific Styling -->
<!-- Additional Styles -->
<% if (typeof drawerExtraStyles !== 'undefined') { %>
<% const drawerStyles = Array.isArray(drawerExtraStyles) ? drawerExtraStyles : [drawerExtraStyles]; %>
<% for (let style of drawerStyles) { %>
<link rel="stylesheet" type="text/css" href="/css/<%= style %>.css">
<% } %>
<% } %>
<!-- END: Additional Styles -->
<!-- END: Styles -->
<!-- Scripts -->
<% if (typeof drawerExtraScripts !== 'undefined') { %>
<% const drawerScripts = Array.isArray(drawerExtraScripts) ? drawerExtraScripts : [drawerExtraScripts]; %>
<% for (let script of drawerScripts) { %>
<% if(typeof script === 'object') { %>
<script type="<%= typeof script.module === 'boolean' && script.module ? 'module' : 'application/javascript' %>" src="<%= script.script %>"></script>
<% } else if(typeof script === 'string' && (script.startsWith('http') || script.startsWith('https'))) { %>
<script type="<%= script.endsWith('.mjs') ? 'module' : 'application/javascript' %>" src="<%= script %>"></script>
<% } else if(typeof script === 'string') { %>
<script type="<%= script.endsWith('.mjs') ? 'module' : 'application/javascript' %>" src="/js/<%= script.endsWith('.mjs') ? script : script + '.js' %>"></script>
<% } %>
<% } %>
<% } %>
<!-- END: Scripts -->
<div id="<%= id %>" class="drawer">
<div id="<%= id %>-drawer-header" role="button" class="drawer-header" tabindex="0" aria-expanded="false" aria-controls="<%= id %>-drawer-contents">
<slot name="drawer-header"></slot>
</div>
<div id="<%= id %>-drawer-contents" class="drawer-contents hidden">
<slot name="drawer-contents"></slot>
</div>
</div>
</template>
<div slot="drawer-header">
<%- label %>
</div>
<div slot="drawer-contents">
<%- content %>
</div>
</ba-drawer>

View file

@ -0,0 +1,180 @@
/**
* 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);