import toggle from "../../scripts/modules/toggleUtil"; interface TableConfig { expandButtonSelector: string; scrollableContainerSelector: string; responsiveTableSelector: string; } export const defaultConfig: TableConfig = { expandButtonSelector: "[data-subrow-toggle]", scrollableContainerSelector: ".table-scrollable", responsiveTableSelector: `[data-table="responsive"]`, }; export default class Table { private element: HTMLElement; private config: TableConfig; private expandButtons: HTMLElement[]; private scrollableContainer: HTMLElement | null; private resizeObserver: ResizeObserver; constructor(element: HTMLElement, config?: Partial) { this.element = element; this.config = { ...defaultConfig, ...config }; this.addRole = this.addRole.bind(this); this.addARIA = this.addARIA.bind(this); this.onExpandClick = this.onExpandClick.bind(this); this.expandButtons = []; this.scrollableContainer = null; (this.element as any).ODS_Table = this; this.resizeObserver = new ResizeObserver((entries) => { if (!this.scrollableContainer) return; const containerRect = this.scrollableContainer.getBoundingClientRect(); entries.forEach((entry) => { if (entry.contentRect.width > containerRect.width) { this.scrollableContainer?.setAttribute("tabindex", "0"); } else { this.scrollableContainer?.removeAttribute("tabindex"); } }); }); this.init(); return this; } private addRole(selector: string, role: string): void { const items = Array.from(this.element.querySelectorAll(selector)); items.forEach((item) => { item.setAttribute("role", role); }); } private addARIA(): void { try { this.element.setAttribute("role", "table"); this.addRole("thead, tbody, tfoot", "rowgroup"); this.addRole("tr", "row"); this.addRole("td", "cell"); this.addRole("th", "columnheader"); this.addRole("th[scope=row]", "rowheader"); // caption role not needed as it is not a real role and // browsers do not dump their own role with display block } catch (e) { console.log("Table.addARIA(): " + e); } } private onExpandClick(e: Event): void { const button = e.currentTarget as HTMLElement; const ariaControls = button.getAttribute("aria-controls") ?? ""; const controlRows = this.element.querySelectorAll( ariaControls .split(" ") .map((id) => `#${id}`) .join(","), ); toggle({ element: button, attribute: "aria-expanded", }); controlRows.forEach((row) => { toggle({ element: row as HTMLElement, attribute: "hidden", }); }); } private init(): void { this.expandButtons = Array.from( this.element.querySelectorAll( this.config.expandButtonSelector, ), ); this.expandButtons.forEach((button) => { button.addEventListener("click", this.onExpandClick); }); if (this.element.matches(this.config.responsiveTableSelector)) { this.addARIA(); } this.maybeInitScrollable(); } private maybeInitScrollable(): boolean | undefined { this.scrollableContainer = this.element.closest( this.config.scrollableContainerSelector, ); if (this.scrollableContainer) { this.resizeObserver.observe(this.element); return true; } return undefined; } public destroy(): void { this.expandButtons.forEach((button) => { button.removeEventListener("click", this.onExpandClick); }); this.resizeObserver.disconnect(); this.expandButtons = []; } public update(): void { this.destroy(); this.init(); } static getInstance(el: HTMLElement): Table | null { return el && (el as any).ODS_Table ? (el as any).ODS_Table : null; } }