+ <% } %>
+
\ No newline at end of file
diff --git a/src/components/dropdown/dropdown.mjs b/src/components/dropdown/dropdown.mjs
new file mode 100644
index 0000000..cd3a2dd
--- /dev/null
+++ b/src/components/dropdown/dropdown.mjs
@@ -0,0 +1,211 @@
+import { ComposableElement } from '../ComposableElement.mjs';
+
+/**
+ * Dropdown Web Component
+ *
+ * A simple dropdown component that toggles the visibility of its content when the "trigger" is clicked or activated with the keyboard.
+ */
+export class Dropdown extends ComposableElement {
+ constructor() {
+ super();
+
+ // Event handler bindings for toggle (popover) and keydown events
+ this._handleToggle = this._toggled.bind(this);
+ this._handleKeyDown = this._keyDown.bind(this)
+ }
+
+ // ----------------------
+ // Private Event Handlers
+ // ----------------------
+
+ /** Content toggled handler (popover content toggled) */
+ _toggled(event) {
+ //event.preventDefault();
+
+ this.currentElemIndex = -1;
+
+ const triggerElem = this.shadow.querySelector(`[aria-controls="${event.currentTarget.id}"]`);
+ const isOpen = event.newState === 'open';
+
+ triggerElem.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
+ event.currentTarget.toggleAttribute('hidden');
+ }
+
+ /** Keydown handler for the dropdown (arrow key navigation) */
+ _keyDown(event) {
+ if(event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
+ return;
+ }
+
+ event.preventDefault();
+
+ const focusableItems = Array.from(this.querySelectorAll('li'));
+
+ if (focusableItems.length === 0) {
+ return;
+ }
+
+ if (event.key === 'ArrowDown') {
+ if(this.currentElemIndex === undefined || this.currentElemIndex == null || this.currentElemIndex >= focusableItems.length - 1) {
+ this.currentElemIndex = -1;
+ }
+
+ this.currentElemIndex++;
+
+ focusableItems[this.currentElemIndex].children[0].focus();
+ }
+ else if(event.key === 'ArrowUp') {
+ if(this.currentElemIndex === undefined || this.currentElemIndex == null || this.currentElemIndex <= 0) {
+ this.currentElemIndex = focusableItems.length;
+ }
+
+ this.currentElemIndex--;
+
+ focusableItems[this.currentElemIndex].children[0].focus();
+ }
+ }
+
+ // -------------------------------
+ // 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 dropdown = this.shadow.querySelector('[popover]');
+ if (dropdown) {
+ // The index of the currently focused element within the dropdown menu, used mainly for keyboard navigation
+ this.currentElemIndex = -1;
+
+ // Add event listeners to the dropdown trigger (button)
+ dropdown.addEventListener('toggle', this._handleToggle);
+ dropdown.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 dropdown = this.shadow.querySelector('[popover]');
+ if (dropdown) {
+ dropdown.removeEventListener('toggle', this._handleToggle);
+ dropdown.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) {
+ const config = this.initializeComponent('dropdown', shadow);
+
+ const cssAnchorPolyfill = document.createElement('script');
+ cssAnchorPolyfill.type = 'module';
+ cssAnchorPolyfill.src = 'https://unpkg.com/@oddbird/css-anchor-positioning/dist/css-anchor-positioning.js';
+ shadow.appendChild(cssAnchorPolyfill);
+
+ // Create the trigger div for the dropdown component
+ const dropdownTriggerDiv = document.createElement('div');
+ dropdownTriggerDiv.role = 'button';
+ dropdownTriggerDiv.tabIndex = 0;
+ dropdownTriggerDiv.setAttribute('aria-expanded', 'false');
+ dropdownTriggerDiv.setAttribute('aria-controls', config.menuId);
+ dropdownTriggerDiv.setAttribute('aria-haspopup', 'true');
+ dropdownTriggerDiv.setAttribute('popovertarget', config.menuId);
+ dropdownTriggerDiv.classList.add('dropdown-trigger');
+ config.triggerStyleClasses.forEach(cls => dropdownTriggerDiv.classList.add(cls));
+
+ // Create the slot for the dropdown trigger content
+ const dropdownTriggerSlotElem = document.createElement('slot');
+ dropdownTriggerSlotElem.name = 'dropdown-trigger';
+
+ dropdownTriggerDiv.appendChild(dropdownTriggerSlotElem);
+
+ // Define the SVG once to keep the HTML clean
+ const dropdownTriggerChevronSvg = `
+ `;
+
+ if((typeof config.desktopOnly === 'boolean' && config.desktopOnly) || (typeof config.mobileOnly === 'boolean' && config.mobileOnly)) {
+ const dropdownTriggerChevronWrapperElem = document.createElement('div');
+
+ if(typeof config.desktopOnly === 'boolean' && config.desktopOnly) {
+ dropdownTriggerChevronWrapperElem.classList.add('desktop-only');
+ }
+ else if(typeof config.mobileOnly === 'boolean' && config.mobileOnly) {
+ dropdownTriggerChevronWrapperElem.classList.add('mobile-only');
+ }
+
+ dropdownTriggerChevronWrapperElem.innerHTML = dropdownTriggerChevronSvg;
+
+ dropdownTriggerDiv.appendChild(dropdownTriggerChevronWrapperElem);
+ }
+ else {
+ dropdownTriggerDiv.innerHTML = dropdownTriggerDiv.innerHTML + dropdownTriggerChevronSvg;
+ }
+
+ // Append the trigger div to the shadow DOM of the component
+ shadow.appendChild(dropdownTriggerDiv);
+
+ // Create the list (`ul`) that will contain the dropdown content
+ const dropdownContentsDiv = document.createElement('div');
+ dropdownContentsDiv.id = config.menuId;
+ dropdownContentsDiv.classList.add('dropdown-menu')
+ config.menuStyleClasses.forEach(cls => dropdownContentsDiv.classList.add(cls));
+ dropdownContentsDiv.hidden = true;
+ dropdownContentsDiv.popover = true;
+
+ const contentSlotElem = document.createElement('slot');
+ contentSlotElem.name = 'dropdown-menu';
+
+ dropdownContentsDiv.appendChild(contentSlotElem);
+
+ shadow.appendChild(dropdownContentsDiv);
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ if (!customElements.get('ba-dropdown')) {
+ customElements.define('ba-dropdown', Dropdown);
+ }
+});
\ No newline at end of file
diff --git a/test-harness/tests/drawer.spec.ts-snapshots/drawer-closed-chromium-linux.png b/test-harness/tests/drawer.spec.ts-snapshots/drawer-closed-chromium-linux.png
index 0dc9d0f..7cfe372 100644
Binary files a/test-harness/tests/drawer.spec.ts-snapshots/drawer-closed-chromium-linux.png and b/test-harness/tests/drawer.spec.ts-snapshots/drawer-closed-chromium-linux.png differ
diff --git a/test-harness/tests/drawer.spec.ts-snapshots/drawer-open-chromium-linux.png b/test-harness/tests/drawer.spec.ts-snapshots/drawer-open-chromium-linux.png
index c72a87d..f24b4ce 100644
Binary files a/test-harness/tests/drawer.spec.ts-snapshots/drawer-open-chromium-linux.png and b/test-harness/tests/drawer.spec.ts-snapshots/drawer-open-chromium-linux.png differ
diff --git a/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-button-closed-chromium-linux.png b/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-button-closed-chromium-linux.png
index 487d1cf..af8012b 100644
Binary files a/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-button-closed-chromium-linux.png and b/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-button-closed-chromium-linux.png differ
diff --git a/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-container-open-chromium-linux.png b/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-container-open-chromium-linux.png
index 487d1cf..af8012b 100644
Binary files a/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-container-open-chromium-linux.png and b/test-harness/tests/tooltip.spec.ts-snapshots/tooltip-container-open-chromium-linux.png differ