UNPKG

@ulu/frontend

Version:

A versatile SCSS and JavaScript component library offering configurable, accessible components and flexible integration into any project, with SCSS modules suitable for modern JS frameworks.

135 lines (122 loc) 3.96 kB
/** * @module ui/popover */ import { ComponentInitializer } from "../utils/system.js"; import { createFloatingUi } from "../utils/floating-ui.js"; import { Collapsible } from "./collapsible.js"; /** * Popover Component Initializer */ export const initializer = new ComponentInitializer({ type: "popover", baseAttribute: "data-ulu-popover" }); const attrSelectorAnchor = initializer.attributeSelector("trigger-anchor"); const attrSelectorArrow = initializer.attributeSelector("arrow"); const attrContent = initializer.getAttribute("content"); const attrSelectorContent = initializer.attributeSelector("content"); /** * Array of current instances */ export const instances = new WeakMap; // This modules collapsible defaults const collapsibleDefaults = { clickOutsideCloses: true, escapeCloses: true }; /** * Initialize default popover */ export function init() { initializer.init({ key: "trigger", withData: true, events: ["pageModified"], setup({ element, data, initialize }) { if (instances.has(element)) return; const resolved = resolve(element, data); if (!resolved) { initializer.warn("Unable to resolve popover elements for trigger.", element); return; } const { elements, options, floatingOptions } = resolved; instances.set(elements, new Popover(elements, options, floatingOptions)); initialize(); } }); } /** * Find the popover's elements */ export function resolve(trigger, userOptions) { const options = Object.assign({}, userOptions); const content = getContentByTrigger(trigger); const elements = { trigger, content, anchor: trigger.querySelector(attrSelectorAnchor) || trigger, contentArrow: content.querySelector(attrSelectorArrow) }; const floatingOptions = options.floating || {}; delete options.floating; if (content) { return { elements, options, floatingOptions }; } else { initializer.logError("Unable to make popover for", trigger); return false; } } // - grab from aria-controls (optional) // - or from direct sibling // - lastly check the parent container for any children that have the attribute export function getContentByTrigger(trigger) { let content; const ariaControls = trigger.getAttribute("aria-controls"); if (ariaControls) { content = document.getElementById(ariaControls); } else if (trigger?.nextElementSibling?.hasAttribute(attrContent)) { content = trigger.nextElementSibling; // @todo - Consider removing this (non standard, users like this should be using aria-controls) } else { const children = Array.from(trigger.parentNode.children); const triggerIndex = children.findIndex(c => c === trigger); const childrenAfter = children.slice(triggerIndex); content = childrenAfter.find(child => child.matches(attrSelectorContent)); } if (!content) { initializer.logError("Unable to resolve 'content' element for popover", trigger); } return content; } /** * Class that extends Collapsible adding floating-ui for popover behavior */ export class Popover extends Collapsible { constructor(elements, config, floatingOptions) { const options = Object.assign({}, collapsibleDefaults, config); super(elements, options); this.floatingOptions = floatingOptions || {}; } setState(isOpen, event) { super.setState(isOpen, event); this.destroyFloatingInstance(); if (isOpen) { this.createFloatingInstance(); } } destroy() { super.destroy(); this.destroyFloatingInstance(); } createFloatingInstance() { const { content, anchor, contentArrow } = this.elements; const floatingElements = { trigger: anchor, contentArrow, content }; this.floatingCleanup = createFloatingUi(floatingElements, this.floatingOptions); } destroyFloatingInstance() { if (this.floatingCleanup) { this.floatingCleanup(); this.floatingCleanup = null; } } }