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
JavaScript
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);
});
}
}