UNPKG

natura11y

Version:

Natura11y is an open-source design system for creating beautiful web interfaces with a focus on accessible interaction design and front-end development.

143 lines (117 loc) 4.33 kB
import { getFocusableElements } from './utilities/focus'; import { delegateEvent } from './utilities/eventDelegation'; export default class Collapse { // Private properties #activeKeydownHandlers = new Map(); #fallbackTabTargets = new Set(); #resizeObserver = new ResizeObserver((entries) => { entries.forEach(({ target }) => { if (getComputedStyle(target).visibility === 'visible') { target.inert = false; this.#resizeObserver.unobserve(target); } }); }); // Private methods #handleCollapseClose(button, target, returnFocus = false) { button.setAttribute('aria-expanded', 'false'); target.classList.remove('shown'); target.inert = true; if (this.#fallbackTabTargets.has(target)) { target.removeAttribute('tabindex'); this.#fallbackTabTargets.delete(target); } if (returnFocus) button.focus(); const noAnimation = window.matchMedia('(prefers-reduced-motion: reduce)').matches || !CSS.supports('interpolate-size', 'allow-keywords'); const cleanup = () => { target.removeAttribute('data-active'); const handler = this.#activeKeydownHandlers.get(target); if (handler) { target.removeEventListener('keydown', handler); this.#activeKeydownHandlers.delete(target); } this.#resizeObserver.observe(target); }; if (noAnimation) { cleanup(); } else { target.addEventListener('transitionend', (e) => { if (e.target !== target || e.propertyName !== 'height') return; cleanup(); }, { once: true }); } } #handleCollapseOpen(button, target, focusMode = null) { button.setAttribute('aria-expanded', 'true'); target.setAttribute('data-active', ''); target.inert = false; this.#resizeObserver.unobserve(target); target.classList.add('shown'); if (focusMode === 'first') { const [firstFocusable] = getFocusableElements(target); if (firstFocusable) { firstFocusable.focus(); } else { target.tabIndex = -1; this.#fallbackTabTargets.add(target); target.focus(); } } else if (focusMode === 'panel') { target.tabIndex = -1; this.#fallbackTabTargets.add(target); target.focus(); } } #toggleCollapse = (event) => { const button = event.target.closest('[data-toggle="collapse"]'); if (!button) return; event.preventDefault(); const targetId = button.getAttribute('aria-controls')?.replace(/^#/, ''); const target = document.getElementById(targetId); if (!target) { console.error(`Collapse target "${targetId}" not found.`); return; } const isExpanded = button.getAttribute('aria-expanded') === 'true'; if (isExpanded) { this.#handleCollapseClose(button, target); } else { const focusMode = target.hasAttribute('data-focus-first') ? 'first' : event.detail === 0 ? 'panel' : null; this.#handleCollapseOpen(button, target, focusMode); } const prev = this.#activeKeydownHandlers.get(target); if (prev) target.removeEventListener('keydown', prev); const handler = (e) => { if (e.code === 'Escape') { this.#handleCollapseClose(button, target, true); } }; target.addEventListener('keydown', handler); this.#activeKeydownHandlers.set(target, handler); if (!isExpanded && button.hasAttribute('data-target-close')) { const closeId = button.getAttribute('data-target-close')?.replace(/^#/, ''); const closeTarget = document.getElementById(closeId); const closeButton = document.querySelector(`[aria-controls="${closeId}"]`); if (closeTarget && closeButton) { if (closeButton.getAttribute('aria-expanded') === 'true') { this.#handleCollapseClose(closeButton, closeTarget); } } else { console.error(`Close target "${closeId}" not found.`); } } }; // Public methods init = () => { document.querySelectorAll('.collapse:not(.shown)').forEach((el) => { if (getComputedStyle(el).visibility !== 'visible') { el.inert = true; } this.#resizeObserver.observe(el); }); delegateEvent(document, 'click', '[data-toggle="collapse"]', this.#toggleCollapse); }; }