import { RovingTabindex } from "../../utils/keyboard"; interface AccordionConfig { buttonSelector: string; } const defaultConfig: AccordionConfig = { buttonSelector: "[data-accordion-toggle]", }; export default class Accordion { private element: HTMLElement; private config: AccordionConfig; private rovingTabindex!: RovingTabindex; private buttons!: HTMLElement[]; constructor(element: HTMLElement, config?: Partial) { this.element = element; this.config = { ...defaultConfig, ...config }; this.onFocus = this.onFocus.bind(this); this.onClick = this.onClick.bind(this); (this.element as any).ODS_Accordion = this; this.init(); return this; } private onFocus(e: FocusEvent): void { if (!this.rovingTabindex.isActive) { this.rovingTabindex.init(); } const button = e.currentTarget as HTMLElement; if ( window.location.hash.substring(1) === button.getAttribute("id") && button.getAttribute("aria-expanded") === "false" ) { button.click(); } } private toggleAccordionItem(button: HTMLElement): void { const sectionId = button.getAttribute("aria-controls"); const section = sectionId ? this.element.querySelector(`#${sectionId}`) : null; if (!section) return; // Get current state const isExpanded = button.getAttribute("aria-expanded") === "true"; const newState = !isExpanded; // Update button aria-expanded button.setAttribute("aria-expanded", String(newState)); // Toggle section visibility if (newState) { section.removeAttribute("hidden"); this.scrollTriggerIntoModalView(button); } else { section.setAttribute("hidden", ""); } } private scrollTriggerIntoModalView(button: HTMLElement): void { const modalDialog = button.closest(".modal__dialog"); if (!modalDialog) return; const scrollToTrigger = () => { const dialogRect = modalDialog.getBoundingClientRect(); const buttonRect = button.getBoundingClientRect(); const stickyHeaderOffset = this.getStickyHeaderOffset(modalDialog); const gap = 8; const top = buttonRect.top - dialogRect.top + modalDialog.scrollTop - stickyHeaderOffset - gap; modalDialog.scrollTo({ top: Math.max(0, top), behavior: "smooth", }); }; if (typeof window.requestAnimationFrame === "function") { window.requestAnimationFrame(scrollToTrigger); return; } setTimeout(scrollToTrigger, 0); } private getStickyHeaderOffset(modalDialog: HTMLElement): number { const modalHeader = modalDialog.querySelector(".modal__header"); if (!modalHeader) return 0; const dialogRect = modalDialog.getBoundingClientRect(); const headerRect = modalHeader.getBoundingClientRect(); return Math.max(0, headerRect.bottom - dialogRect.top); } private init(): void { this.buttons = Array.from( this.element.querySelectorAll(this.config.buttonSelector), ); if (!this.rovingTabindex) { this.rovingTabindex = new RovingTabindex(this.buttons, { direction: "vertical", }); } else { this.rovingTabindex.update(this.buttons); } this.buttons.forEach((button) => { button.addEventListener("focus", this.onFocus); button.addEventListener("click", this.onClick); }); } private onClick(e: Event): void { const button = e.currentTarget as HTMLElement; this.toggleAccordionItem(button); } destroy(): void { this.rovingTabindex.destroy(); this.buttons.forEach((button) => { button.removeEventListener("focus", this.onFocus); button.removeEventListener("click", this.onClick); }); } update(): void { this.destroy(); this.init(); } static getInstance(el: HTMLElement): Accordion | null { return el && (el as any).ODS_Accordion ? (el as any).ODS_Accordion : null; } }