class CartNotification extends HTMLElement { constructor() { super(); this.notification = document.getElementById('cart-notification'); this.header = document.querySelector('sticky-header'); this.onBodyClick = this.handleBodyClick.bind(this); this.notification.addEventListener('keyup', (evt) => evt.code === 'Escape' && this.close()); this.querySelectorAll('button[type="button"]').forEach((closeButton) => closeButton.addEventListener('click', this.close.bind(this)) ); } open() { this.notification.classList.add('animate', 'active'); this.notification.addEventListener( 'transitionend', () => { this.notification.focus(); trapFocus(this.notification); }, { once: true } ); document.body.addEventListener('click', this.onBodyClick); this.dispatchCartViewEvent(); } // The notification's outer element is server-rendered once at page load, so // its `cart` Liquid object reflects the pre-add state. The morphed children // (inserted from the /cart/add.js sections response in renderContents) are // post-add, but they don't expose the full cart shape we need for the event // payload. So we keep an explicit /cart.json fetch on open. Migrating to the // factory + filter would require re-rendering the notification element // itself in sections, which is out of scope for this PR. async dispatchCartViewEvent() { const { CartViewEvent } = window.StandardEvents || {}; if (!CartViewEvent) return; try { const response = await fetch(`${routes.cart_url}.json`); const cart = await response.json(); if (!cart?.currency) return; this.dispatchEvent( new CartViewEvent({ context: 'dialog', cart: CartViewEvent.createCartFromAjaxResponse(cart), }) ); } catch (e) { // cart:view is informational; swallow fetch errors } } close() { this.notification.classList.remove('active'); document.body.removeEventListener('click', this.onBodyClick); removeTrapFocus(this.activeElement); } renderContents(parsedState) { this.cartItemKey = parsedState.key; this.getSectionsToRender().forEach((section) => { document.getElementById(section.id).innerHTML = this.getSectionInnerHTML( parsedState.sections[section.id], section.selector ); }); if (this.header) this.header.reveal(); this.open(); } getSectionsToRender() { return [ { id: 'cart-notification-product', selector: `[id="cart-notification-product-${this.cartItemKey}"]`, }, { id: 'cart-notification-button', }, { id: 'cart-icon-bubble', }, ]; } getSectionInnerHTML(html, selector = '.shopify-section') { return new DOMParser().parseFromString(html, 'text/html').querySelector(selector).innerHTML; } handleBodyClick(evt) { const target = evt.target; if (target !== this.notification && !target.closest('cart-notification')) { const disclosure = target.closest('details-disclosure, header-menu'); this.activeElement = disclosure ? disclosure.querySelector('summary') : null; this.close(); } } setActiveElement(element) { this.activeElement = element; } } customElements.define('cart-notification', CartNotification);