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.

170 lines (125 loc) 4.93 kB
import { handleOverlayOpen, handleOverlayClose } from './utilities/overlay'; import { delegateEvent } from './utilities/eventDelegation'; export default class Flyout { // Private properties #escapeKeyHandler = this.#handleEscapeKey.bind(this); #backdropClickHandler = null; #panelStacks = new WeakMap(); // Private methods #handleEscapeKey(event) { if (event.code === 'Escape') { const openMenu = document.querySelector('.flyout.shown'); if (openMenu) this.#handleMenuClose(openMenu); } } #handleMenuOpen(menu) { const content = menu.querySelector('.flyout__content'); const closeBtn = content?.querySelector('[data-flyout-close]'); if (!content) return; this.#resetPanels(menu); menu.classList.add('shown'); menu.setAttribute('aria-hidden', 'false'); handleOverlayOpen(content, null, closeBtn); this.#backdropClickHandler = (event) => { if (!content.contains(event.target)) { this.#handleMenuClose(menu); } }; window.addEventListener('pointerdown', this.#backdropClickHandler); window.addEventListener('keydown', this.#escapeKeyHandler); } #handleMenuClose(menu) { menu.classList.remove('shown'); handleOverlayClose(menu); if (this.#backdropClickHandler) { window.removeEventListener('pointerdown', this.#backdropClickHandler); this.#backdropClickHandler = null; } window.removeEventListener('keydown', this.#escapeKeyHandler); } #resetPanels(menu) { menu.querySelectorAll('.flyout__panel').forEach((panel, index) => { if (index === 0) { panel.removeAttribute('inert'); } else { panel.setAttribute('inert', ''); } }); menu.querySelector('.flyout__header [data-flyout-back]')?.setAttribute('hidden', ''); this.#panelStacks.delete(menu); } #animateEnter(el) { el.setAttribute('data-entering', ''); el.addEventListener('animationend', () => el.removeAttribute('data-entering'), { once: true }); } #navigateNext(menu, targetId) { const allPanels = [...menu.querySelectorAll('.flyout__panel')]; const target = menu.querySelector(`#${targetId}`); if (!target) return; const targetIndex = allPanels.indexOf(target); if (targetIndex === -1) return; if (!this.#panelStacks.has(menu)) this.#panelStacks.set(menu, []); const currentPanel = allPanels.find(p => !p.hasAttribute('inert')) ?? allPanels[0]; const currentIndex = allPanels.indexOf(currentPanel); this.#panelStacks.get(menu).push(currentIndex); currentPanel.setAttribute('inert', ''); target.removeAttribute('inert'); this.#animateEnter(target); const backBtn = menu.querySelector('.flyout__header [data-flyout-back]'); if (backBtn?.hidden) { backBtn.removeAttribute('hidden'); this.#animateEnter(backBtn); } backBtn?.focus({ preventScroll: true }); } #navigateBack(menu) { if (!this.#panelStacks.has(menu)) return; const stack = this.#panelStacks.get(menu); if (!stack.length) return; const allPanels = [...menu.querySelectorAll('.flyout__panel')]; const currentPanel = allPanels.find(p => !p.hasAttribute('inert')) ?? allPanels[0]; const prevIndex = stack.pop(); const prevPanel = allPanels[prevIndex]; currentPanel.setAttribute('inert', ''); prevPanel.removeAttribute('inert'); this.#animateEnter(prevPanel); if (prevIndex === 0) { menu.querySelector('.flyout__header [data-flyout-back]')?.setAttribute('hidden', ''); } menu.querySelector('[data-flyout-close]')?.focus({ preventScroll: true }); } // Public methods init() { document.querySelectorAll('.flyout').forEach(menu => this.#resetPanels(menu)); delegateEvent(document, 'click', '[data-open="flyout"]', (event) => { const trigger = event.target.closest('[data-open="flyout"]'); const menuId = trigger?.getAttribute('aria-controls')?.replace(/^#/, ''); const menu = document.getElementById(menuId); if (menu) this.#handleMenuOpen(menu); }); delegateEvent(document, 'click', '[data-flyout-close]', (event) => { const menu = event.target.closest('.flyout'); if (menu) this.#handleMenuClose(menu); }); delegateEvent(document, 'click', '[data-flyout-next]', (event) => { const menu = event.target.closest('.flyout'); const btn = event.target.closest('[data-flyout-next]'); const targetId = btn?.getAttribute('aria-controls')?.replace(/^#/, ''); if (menu && targetId) this.#navigateNext(menu, targetId); }); delegateEvent(document, 'click', '[data-flyout-back]', (event) => { const menu = event.target.closest('.flyout'); if (menu) this.#navigateBack(menu); }); document.querySelectorAll('[data-open="flyout"]').forEach(trigger => { const menuId = trigger.getAttribute('aria-controls')?.replace(/^#/, ''); const menu = document.getElementById(menuId); if (!menu) return; new ResizeObserver(() => { if (menu.classList.contains('shown') && !trigger.offsetWidth) { this.#handleMenuClose(menu); } }).observe(trigger); }); } }