import { transitionHiddenElement } from "@cloudfour/transition-hidden-element"; import type { Instance, Options } from "@popperjs/core"; import { createPopper } from "@popperjs/core"; import { keys } from "../../utils"; interface DropdownConfig { dataInteractive?: string; focusTrap?: string; removeDialogOnDestroy?: boolean; popperOptions?: Partial & { modifiers?: any[] }; } const defaultConfig: Required = { dataInteractive: "data-dropdown-interactive", focusTrap: "data-dropdown-focus-trap", removeDialogOnDestroy: false, popperOptions: { placement: "bottom-start", strategy: "absolute", modifiers: [ { name: "eventListeners", options: { scroll: false, resize: false, }, }, { name: "flip", options: { fallbackPlacements: ["bottom-end", "bottom-start", "bottom"], }, }, { name: "offset", options: { offset: [0, -1], }, }, ], }, }; export default class Dropdown { element!: HTMLElement; config!: Required; trigger!: HTMLElement; instance!: Instance; transitioner: any; focusableElements!: NodeListOf; constructor(element: HTMLElement, config: DropdownConfig = {}) { if ((element as any).ODS_Dropdown) { return (element as any).ODS_Dropdown; } this.element = element; this.config = { ...defaultConfig, ...config }; this.config.popperOptions = { ...defaultConfig.popperOptions, ...(config?.popperOptions || {}), }; this.config.popperOptions.modifiers = [ ...((defaultConfig.popperOptions.modifiers as any[]) || []), ...((config?.popperOptions?.modifiers as any[]) || []), ]; this.handleVisibility = this.handleVisibility.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); this.handleClick = this.handleClick.bind(this); (this.element as any).ODS_Dropdown = this; this.focusableElements = this.element.querySelectorAll( `a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])`, ); this.init(); return this; } static getInstance(el: HTMLElement): Dropdown | null { return el && (el as any).ODS_Dropdown ? (el as any).ODS_Dropdown : null; } init() { this.trigger = document.getElementById( this.element.id.replace("dropdown", "btn"), ) as HTMLElement; const customPlacement = this.element.getAttribute( "data-dropdown-placement", ); if (customPlacement) { this.config.popperOptions.placement = customPlacement as any; } this.instance = createPopper( this.trigger, this.element, this.config.popperOptions, ); this.transitioner = transitionHiddenElement({ element: this.element, visibleClass: "dropdown--visible", }); this.trigger.addEventListener("click", this.handleVisibility, true); } handleVisibility() { if (this.element.classList.contains("dropdown--visible")) { this.hide(); } else { this.show(); } } handleClick(e: MouseEvent) { if (this.trigger !== e.target && !this.trigger.contains(e.target as Node)) { if ( !this.element.hasAttribute(this.config.dataInteractive) || (e.target as HTMLElement).id.includes( this.element.id.replace("dropdown", "btn"), ) || !this.element.contains(e.target as Node) ) { this.handleVisibility(); } } } handleKeyPress(e: KeyboardEvent) { if (keys.ESC.includes(e.key)) { this.handleVisibility(); if (this.element.contains(document.activeElement)) { this.trigger.focus(); } this.hide(); } if (this.element.hasAttribute(this.config.focusTrap)) { const firstFocusableEl = this.focusableElements[0]; const lastFocusableEl = this.focusableElements[this.focusableElements.length - 1]; if ( document.activeElement === (this as any).button && (e.key === "Tab" || e.keyCode === 9) ) { if (e.shiftKey) { this.focusableElements[this.focusableElements.length - 1].focus(); e.preventDefault(); } } else if (e.key === "Tab" || e.keyCode === 9) { if (e.shiftKey) { if (document.activeElement === firstFocusableEl) { lastFocusableEl.focus(); e.preventDefault(); } } else { if (document.activeElement === lastFocusableEl) { firstFocusableEl.focus(); e.preventDefault(); } } } } } show() { this.transitioner.show(); this.instance.forceUpdate(); document.addEventListener("keydown", this.handleKeyPress); document.addEventListener("click", this.handleClick); window.onresize = () => this.update(); } hide() { this.transitioner.hide(); document.removeEventListener("keydown", this.handleKeyPress); document.removeEventListener("click", this.handleClick); window.onresize = null; } update() { this.instance.forceUpdate(); } destroy() { this.trigger.removeEventListener("click", this.handleVisibility, true); this.hide(); this.instance.destroy(); (this.element as any).ODS_Dropdown = null; window.onresize = null; if (this.config.removeDialogOnDestroy) { this.element.remove(); } } }