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