@magic-spells/cart-panel 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,6 @@ cart-item {
4
4
  --cart-item-appearing-duration: 400ms;
5
5
  --cart-item-shadow-color: rgba(0, 0, 0, 0.15);
6
6
  --cart-item-shadow-color-strong: rgba(0, 0, 0, 0.5);
7
- --cart-item-processing-bg: rgba(100, 100, 100, 0.2);
8
7
  --cart-item-destroying-bg: rgba(0, 0, 0, 0.1);
9
8
  --cart-item-processing-scale: 0.98;
10
9
  --cart-item-destroying-scale: 0.85;
@@ -35,10 +34,14 @@ cart-item::after {
35
34
  left: 0px;
36
35
  transition: background-color var(--cart-item-processing-duration) ease;
37
36
  }
37
+ cart-item[state=ready] {
38
+ transition: filter var(--cart-item-processing-duration) ease-out, background-color var(--cart-item-processing-duration) ease-out, box-shadow var(--cart-item-processing-duration) ease-out, height var(--cart-item-appearing-duration) ease-out;
39
+ }
38
40
  cart-item[state=ready] cart-item-content {
39
41
  transform: scale(1);
40
42
  filter: blur(0px);
41
43
  opacity: 1;
44
+ transition: transform var(--cart-item-appearing-duration) ease-out, filter var(--cart-item-appearing-duration) ease-out, opacity var(--cart-item-appearing-duration) ease-out;
42
45
  }
43
46
  cart-item[state=ready] cart-item-processing {
44
47
  opacity: 0;
@@ -55,6 +58,7 @@ cart-item[state=processing] cart-item-content {
55
58
  filter: blur(var(--cart-item-processing-blur));
56
59
  opacity: 0.9;
57
60
  pointer-events: none;
61
+ transition: transform var(--cart-item-processing-duration) ease-out, filter var(--cart-item-processing-duration) ease-out, opacity var(--cart-item-processing-duration) ease-out;
58
62
  }
59
63
  cart-item[state=processing] cart-item-processing {
60
64
  opacity: 1;
@@ -62,7 +66,7 @@ cart-item[state=processing] cart-item-processing {
62
66
  }
63
67
  cart-item[state=destroying] {
64
68
  background-color: var(--cart-item-destroying-bg);
65
- box-shadow: inset 0px 4px 20px var(--cart-item-shadow-color-strong);
69
+ box-shadow: inset 0px 2px 20px var(--cart-item-shadow-color-strong);
66
70
  margin-top: 0px;
67
71
  margin-bottom: 0px;
68
72
  transition: filter var(--cart-item-destroying-duration) ease, background-color var(--cart-item-destroying-duration) ease, box-shadow var(--cart-item-destroying-duration) ease, margin var(--cart-item-destroying-duration) ease;
@@ -100,7 +104,6 @@ cart-item[state=appearing] cart-item-processing {
100
104
 
101
105
  cart-item-content {
102
106
  display: block;
103
- transition: transform var(--cart-item-processing-duration) ease-out, filter var(--cart-item-processing-duration) ease-out, opacity var(--cart-item-processing-duration) ease-out;
104
107
  }
105
108
 
106
109
  cart-item-processing {
@@ -186,8 +189,7 @@ cart-panel {
186
189
  top: 0;
187
190
  right: 0;
188
191
  width: var(--cart-panel-width);
189
- height: 100vh;
190
- opacity: 0;
192
+ height: 100dvh;
191
193
  transform: translateX(100%);
192
194
  pointer-events: none;
193
195
  z-index: var(--cart-panel-z-index);
@@ -195,7 +197,7 @@ cart-panel {
195
197
  box-shadow: var(--cart-panel-shadow);
196
198
  border-radius: var(--cart-panel-border-radius);
197
199
  overflow: hidden;
198
- transition: opacity var(--cart-transition-duration) var(--cart-transition-timing), transform var(--cart-transition-duration) var(--cart-transition-timing);
200
+ transition: transform var(--cart-transition-duration) var(--cart-transition-timing);
199
201
  }
200
202
  cart-panel.hidden {
201
203
  display: none;
@@ -203,10 +205,4 @@ cart-panel.hidden {
203
205
 
204
206
  body.overflow-hidden {
205
207
  overflow: hidden;
206
- position: fixed;
207
- width: 100%;
208
- height: 100%;
209
- left: 0;
210
- right: 0;
211
- margin: 0;
212
208
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- require('@magic-spells/cart-item');
5
+ var cartItem = require('@magic-spells/cart-item');
6
6
  require('@magic-spells/focus-trap');
7
7
  var EventEmitter = require('@magic-spells/event-emitter');
8
8
 
@@ -12,9 +12,9 @@ var EventEmitter = require('@magic-spells/event-emitter');
12
12
  */
13
13
  class CartDialog extends HTMLElement {
14
14
  #handleTransitionEnd;
15
- #scrollPosition = 0;
16
15
  #currentCart = null;
17
16
  #eventEmitter;
17
+ #isInitialRender = true;
18
18
 
19
19
  /**
20
20
  * Clean up event listeners when component is removed from DOM
@@ -34,31 +34,21 @@ class CartDialog extends HTMLElement {
34
34
  }
35
35
 
36
36
  /**
37
- * Saves current scroll position and locks body scrolling
37
+ * Locks body scrolling
38
38
  * @private
39
39
  */
40
40
  #lockScroll() {
41
- const _ = this;
42
- // Save current scroll position
43
- _.#scrollPosition = window.pageYOffset;
44
-
45
- // Apply fixed position to body
41
+ // Apply overflow hidden to body
46
42
  document.body.classList.add('overflow-hidden');
47
- document.body.style.top = `-${_.#scrollPosition}px`;
48
43
  }
49
44
 
50
45
  /**
51
- * Restores scroll position when cart dialog is closed
46
+ * Restores body scrolling when cart dialog is closed
52
47
  * @private
53
48
  */
54
49
  #restoreScroll() {
55
- const _ = this;
56
- // Remove fixed positioning
50
+ // Remove overflow hidden from body
57
51
  document.body.classList.remove('overflow-hidden');
58
- document.body.style.removeProperty('top');
59
-
60
- // Restore scroll position
61
- window.scrollTo(0, _.#scrollPosition);
62
52
  }
63
53
 
64
54
  /**
@@ -99,7 +89,18 @@ class CartDialog extends HTMLElement {
99
89
  return;
100
90
  }
101
91
 
102
- _.focusTrap = document.createElement('focus-trap');
92
+ // Check if focus-trap already exists, if not create one
93
+ _.focusTrap = _.contentPanel.querySelector('focus-trap');
94
+ if (!_.focusTrap) {
95
+ _.focusTrap = document.createElement('focus-trap');
96
+
97
+ // Move all existing cart-panel content into the focus trap
98
+ const existingContent = Array.from(_.contentPanel.childNodes);
99
+ existingContent.forEach((child) => _.focusTrap.appendChild(child));
100
+
101
+ // Insert focus trap inside the cart-panel
102
+ _.contentPanel.appendChild(_.focusTrap);
103
+ }
103
104
 
104
105
  // Ensure we have labelledby and describedby references
105
106
  if (!_.getAttribute('aria-labelledby')) {
@@ -112,15 +113,15 @@ class CartDialog extends HTMLElement {
112
113
  }
113
114
  }
114
115
 
115
- _.contentPanel.parentNode.insertBefore(_.focusTrap, _.contentPanel);
116
- _.focusTrap.appendChild(_.contentPanel);
117
-
118
- _.focusTrap.setupTrap();
119
-
120
- // Add modal overlay
121
- _.prepend(document.createElement('cart-overlay'));
116
+ // Add modal overlay if it doesn't already exist
117
+ if (!_.querySelector('cart-overlay')) {
118
+ _.prepend(document.createElement('cart-overlay'));
119
+ }
122
120
  _.#attachListeners();
123
121
  _.#bindKeyboard();
122
+
123
+ // Load cart data immediately after component initialization
124
+ _.refreshCart();
124
125
  }
125
126
 
126
127
  /**
@@ -153,6 +154,14 @@ class CartDialog extends HTMLElement {
153
154
  */
154
155
  #emit(eventName, data = null) {
155
156
  this.#eventEmitter.emit(eventName, data);
157
+
158
+ // Also emit as native DOM events for better compatibility
159
+ this.dispatchEvent(
160
+ new CustomEvent(eventName, {
161
+ detail: data,
162
+ bubbles: true,
163
+ })
164
+ );
156
165
  }
157
166
 
158
167
  /**
@@ -176,7 +185,7 @@ class CartDialog extends HTMLElement {
176
185
 
177
186
  // Handle close buttons
178
187
  _.addEventListener('click', (e) => {
179
- if (!e.target.closest('[data-action="hide-cart"]')) return;
188
+ if (!e.target.closest('[data-action-hide-cart]')) return;
180
189
  _.hide();
181
190
  });
182
191
 
@@ -231,14 +240,15 @@ class CartDialog extends HTMLElement {
231
240
  this.updateCartItem(cartKey, 0)
232
241
  .then((updatedCart) => {
233
242
  if (updatedCart && !updatedCart.error) {
234
- // Success - remove with animation
235
- element.destroyYourself();
243
+ // Success - let smart comparison handle the removal animation
236
244
  this.#currentCart = updatedCart;
237
- this.#updateCartItems(updatedCart);
245
+ this.#renderCartItems(updatedCart);
246
+ this.#renderCartPanel(updatedCart);
238
247
 
239
248
  // Emit cart updated and data changed events
240
- this.#emit('cart-dialog:updated', { cart: updatedCart });
241
- this.#emit('cart-dialog:data-changed', updatedCart);
249
+ const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
250
+ this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
251
+ this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
242
252
  } else {
243
253
  // Error - reset to ready state
244
254
  element.setState('ready');
@@ -266,14 +276,16 @@ class CartDialog extends HTMLElement {
266
276
  this.updateCartItem(cartKey, quantity)
267
277
  .then((updatedCart) => {
268
278
  if (updatedCart && !updatedCart.error) {
269
- // Success - update cart data
279
+ // Success - update cart data and refresh items
270
280
  this.#currentCart = updatedCart;
271
- this.#updateCartItems(updatedCart);
281
+ this.#renderCartItems(updatedCart);
282
+ this.#renderCartPanel(updatedCart);
272
283
  element.setState('ready');
273
284
 
274
285
  // Emit cart updated and data changed events
275
- this.#emit('cart-dialog:updated', { cart: updatedCart });
276
- this.#emit('cart-dialog:data-changed', updatedCart);
286
+ const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
287
+ this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
288
+ this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
277
289
  } else {
278
290
  // Error - reset to ready state
279
291
  element.setState('ready');
@@ -288,13 +300,82 @@ class CartDialog extends HTMLElement {
288
300
  }
289
301
 
290
302
  /**
291
- * Update cart items
303
+ * Update cart count elements across the site
304
+ * @private
305
+ */
306
+ #renderCartCount(cartData) {
307
+ if (!cartData) return;
308
+
309
+ // Calculate visible item count (excluding _hide_in_cart items)
310
+ const visibleItems = this.#getVisibleCartItems(cartData);
311
+ const visibleItemCount = visibleItems.reduce((total, item) => total + item.quantity, 0);
312
+
313
+ // Update all cart count elements across the site
314
+ const cartCountElements = document.querySelectorAll('[data-content-cart-count]');
315
+ cartCountElements.forEach((element) => {
316
+ element.textContent = visibleItemCount;
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Update cart subtotal elements across the site
322
+ * @private
323
+ */
324
+ #renderCartSubtotal(cartData) {
325
+ if (!cartData) return;
326
+
327
+ // Calculate subtotal from all items except those marked to ignore pricing
328
+ const pricedItems = cartData.items.filter(item => {
329
+ const ignorePrice = item.properties?._ignore_price_in_subtotal;
330
+ return !ignorePrice;
331
+ });
332
+ const subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
333
+
334
+ // Update all cart subtotal elements across the site
335
+ const cartSubtotalElements = document.querySelectorAll('[data-content-cart-subtotal]');
336
+ cartSubtotalElements.forEach((element) => {
337
+ // Format as currency (assuming cents, convert to dollars)
338
+ const formatted = (subtotal / 100).toFixed(2);
339
+ element.textContent = `$${formatted}`;
340
+ });
341
+ }
342
+
343
+ /**
344
+ * Update cart items display based on cart data
292
345
  * @private
293
346
  */
294
- #updateCartItems(cart = null) {
295
- // Placeholder for cart item updates
296
- // Could be used to sync cart items with server data
297
- cart || this.#currentCart;
347
+ #renderCartPanel(cart = null) {
348
+ const cartData = cart || this.#currentCart;
349
+ if (!cartData) return;
350
+
351
+ // Get cart sections
352
+ const hasItemsSection = this.querySelector('[data-cart-has-items]');
353
+ const emptySection = this.querySelector('[data-cart-is-empty]');
354
+ const itemsContainer = this.querySelector('[data-content-cart-items]');
355
+
356
+ if (!hasItemsSection || !emptySection || !itemsContainer) {
357
+ console.warn(
358
+ 'Cart sections not found. Expected [data-cart-has-items], [data-cart-is-empty], and [data-content-cart-items]'
359
+ );
360
+ return;
361
+ }
362
+
363
+ // Check visible item count for showing/hiding sections
364
+ const visibleItems = this.#getVisibleCartItems(cartData);
365
+ const hasVisibleItems = visibleItems.length > 0;
366
+
367
+ // Show/hide sections based on visible item count
368
+ if (hasVisibleItems) {
369
+ hasItemsSection.style.display = '';
370
+ emptySection.style.display = 'none';
371
+ } else {
372
+ hasItemsSection.style.display = 'none';
373
+ emptySection.style.display = '';
374
+ }
375
+
376
+ // Update cart count and subtotal across the site
377
+ this.#renderCartCount(cartData);
378
+ this.#renderCartSubtotal(cartData);
298
379
  }
299
380
 
300
381
  /**
@@ -346,31 +427,216 @@ class CartDialog extends HTMLElement {
346
427
 
347
428
  /**
348
429
  * Refresh cart data from server and update components
430
+ * @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
349
431
  * @returns {Promise<Object>} Cart data object
350
432
  */
351
- refreshCart() {
433
+ refreshCart(cartObj = null) {
434
+ // If cart object is provided, use it directly
435
+ if (cartObj && !cartObj.error) {
436
+ // console.log('Using provided cart data:', cartObj);
437
+ this.#currentCart = cartObj;
438
+ this.#renderCartItems(cartObj);
439
+ this.#renderCartPanel(cartObj);
440
+
441
+ // Emit cart refreshed and data changed events
442
+ const cartWithCalculatedFields = this.#addCalculatedFields(cartObj);
443
+ this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
444
+ this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
445
+
446
+ return Promise.resolve(cartObj);
447
+ }
448
+
449
+ // Otherwise fetch from server
352
450
  return this.getCart().then((cartData) => {
451
+ // console.log('Cart data received:', cartData);
353
452
  if (cartData && !cartData.error) {
354
453
  this.#currentCart = cartData;
355
- this.#updateCartItems(cartData);
454
+ this.#renderCartItems(cartData);
455
+ this.#renderCartPanel(cartData);
356
456
 
357
457
  // Emit cart refreshed and data changed events
358
- this.#emit('cart-dialog:refreshed', { cart: cartData });
359
- this.#emit('cart-dialog:data-changed', cartData);
458
+ const cartWithCalculatedFields = this.#addCalculatedFields(cartData);
459
+ this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
460
+ this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
461
+ } else {
462
+ console.warn('Cart data has error or is null:', cartData);
360
463
  }
361
464
  return cartData;
362
465
  });
363
466
  }
364
467
 
468
+ /**
469
+ * Remove items from DOM that are no longer in cart data
470
+ * @private
471
+ */
472
+ #removeItemsFromDOM(itemsContainer, newKeysSet) {
473
+ const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
474
+
475
+ const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
476
+
477
+ itemsToRemove.forEach((item) => {
478
+ console.log('destroy yourself', item);
479
+ item.destroyYourself();
480
+ });
481
+ }
482
+
483
+ /**
484
+ * Add new items to DOM with animation delay
485
+ * @private
486
+ */
487
+ #addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
488
+ // Delay adding new items by 300ms to let cart slide open first
489
+ setTimeout(() => {
490
+ itemsToAdd.forEach((itemData) => {
491
+ const cartItem$1 = cartItem.CartItem.createAnimated(itemData);
492
+ const targetIndex = newKeys.indexOf(itemData.key || itemData.id);
493
+
494
+ // Find the correct position to insert the new item
495
+ if (targetIndex === 0) {
496
+ // Insert at the beginning
497
+ itemsContainer.insertBefore(cartItem$1, itemsContainer.firstChild);
498
+ } else {
499
+ // Find the item that should come before this one
500
+ let insertAfter = null;
501
+ for (let i = targetIndex - 1; i >= 0; i--) {
502
+ const prevKey = newKeys[i];
503
+ const prevItem = itemsContainer.querySelector(`cart-item[key="${prevKey}"]`);
504
+ if (prevItem) {
505
+ insertAfter = prevItem;
506
+ break;
507
+ }
508
+ }
509
+
510
+ if (insertAfter) {
511
+ insertAfter.insertAdjacentElement('afterend', cartItem$1);
512
+ } else {
513
+ itemsContainer.appendChild(cartItem$1);
514
+ }
515
+ }
516
+ });
517
+ }, 100);
518
+ }
519
+
520
+ /**
521
+ * Filter cart items to exclude those with _hide_in_cart property
522
+ * @private
523
+ */
524
+ #getVisibleCartItems(cartData) {
525
+ if (!cartData || !cartData.items) return [];
526
+ return cartData.items.filter((item) => {
527
+ // Check for _hide_in_cart in various possible locations
528
+ const hidden = item.properties?._hide_in_cart;
529
+
530
+ return !hidden;
531
+ });
532
+ }
533
+
534
+ /**
535
+ * Add calculated fields to cart object for events
536
+ * @private
537
+ */
538
+ #addCalculatedFields(cartData) {
539
+ if (!cartData) return cartData;
540
+
541
+ const visibleItems = this.#getVisibleCartItems(cartData);
542
+ const calculated_count = visibleItems.reduce((total, item) => total + item.quantity, 0);
543
+ const calculated_subtotal = visibleItems.reduce(
544
+ (total, item) => total + (item.line_price || 0),
545
+ 0
546
+ );
547
+
548
+ return {
549
+ ...cartData,
550
+ calculated_count,
551
+ calculated_subtotal,
552
+ };
553
+ }
554
+
555
+ /**
556
+ * Render cart items from Shopify cart data with smart comparison
557
+ * @private
558
+ */
559
+ #renderCartItems(cartData) {
560
+ const itemsContainer = this.querySelector('[data-content-cart-items]');
561
+
562
+ if (!itemsContainer || !cartData || !cartData.items) {
563
+ console.warn('Cannot render cart items:', {
564
+ itemsContainer: !!itemsContainer,
565
+ cartData: !!cartData,
566
+ items: cartData?.items?.length,
567
+ });
568
+ return;
569
+ }
570
+
571
+ // Filter out items with _hide_in_cart property
572
+ const visibleItems = this.#getVisibleCartItems(cartData);
573
+
574
+ // Handle initial render - load all items without animation
575
+ if (this.#isInitialRender) {
576
+ // console.log('Initial cart render:', visibleItems.length, 'visible items');
577
+
578
+ // Clear existing items
579
+ itemsContainer.innerHTML = '';
580
+
581
+ // Create cart-item elements without animation
582
+ visibleItems.forEach((itemData) => {
583
+ const cartItem$1 = new cartItem.CartItem(itemData); // No animation
584
+ // const cartItem = document.createElement('cart-item');
585
+ // cartItem.setData(itemData);
586
+ itemsContainer.appendChild(cartItem$1);
587
+ });
588
+
589
+ this.#isInitialRender = false;
590
+
591
+ return;
592
+ }
593
+
594
+ // Get current DOM items and their keys
595
+ const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
596
+ const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
597
+
598
+ // Get new cart data keys in order (only visible items)
599
+ const newKeys = visibleItems.map((item) => item.key || item.id);
600
+ const newKeysSet = new Set(newKeys);
601
+
602
+ // Step 1: Remove items that are no longer in cart data
603
+ this.#removeItemsFromDOM(itemsContainer, newKeysSet);
604
+
605
+ // Step 2: Add new items that weren't in DOM (with animation delay)
606
+ const itemsToAdd = visibleItems.filter(
607
+ (itemData) => !currentKeys.has(itemData.key || itemData.id)
608
+ );
609
+ this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
610
+ }
611
+
612
+ /**
613
+ * Set the template function for cart items
614
+ * @param {Function} templateFn - Function that takes item data and returns HTML string
615
+ */
616
+ setCartItemTemplate(templateName, templateFn) {
617
+ cartItem.CartItem.setTemplate(templateName, templateFn);
618
+ }
619
+
620
+ /**
621
+ * Set the processing template function for cart items
622
+ * @param {Function} templateFn - Function that returns HTML string for processing state
623
+ */
624
+ setCartItemProcessingTemplate(templateFn) {
625
+ cartItem.CartItem.setProcessingTemplate(templateFn);
626
+ }
627
+
365
628
  /**
366
629
  * Shows the cart dialog and traps focus within it
367
630
  * @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
368
631
  * @fires CartDialog#show - Fired when the cart dialog has been shown
369
632
  */
370
- show(triggerEl = null) {
633
+ show(triggerEl = null, cartObj) {
371
634
  const _ = this;
372
635
  _.triggerEl = triggerEl || false;
373
636
 
637
+ // Lock body scrolling
638
+ _.#lockScroll();
639
+
374
640
  // Remove the hidden class first to ensure content is rendered
375
641
  _.contentPanel.classList.remove('hidden');
376
642
 
@@ -378,17 +644,16 @@ class CartDialog extends HTMLElement {
378
644
  requestAnimationFrame(() => {
379
645
  // Update ARIA states
380
646
  _.setAttribute('aria-hidden', 'false');
647
+
381
648
  if (_.triggerEl) {
382
649
  _.triggerEl.setAttribute('aria-expanded', 'true');
383
650
  }
384
651
 
385
- // Lock body scrolling and save scroll position
386
- _.#lockScroll();
387
-
388
652
  // Focus management
389
653
  const firstFocusable = _.querySelector(
390
654
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
391
655
  );
656
+
392
657
  if (firstFocusable) {
393
658
  requestAnimationFrame(() => {
394
659
  firstFocusable.focus();
@@ -396,7 +661,7 @@ class CartDialog extends HTMLElement {
396
661
  }
397
662
 
398
663
  // Refresh cart data when showing
399
- _.refreshCart();
664
+ _.refreshCart(cartObj);
400
665
 
401
666
  // Emit show event - cart dialog is now visible
402
667
  _.#emit('cart-dialog:show', { triggerElement: _.triggerEl });
@@ -411,23 +676,31 @@ class CartDialog extends HTMLElement {
411
676
  hide() {
412
677
  const _ = this;
413
678
 
414
- // Restore body scroll and scroll position
415
- _.#restoreScroll();
416
-
417
679
  // Update ARIA states
418
680
  if (_.triggerEl) {
419
681
  // remove focus from modal panel first
420
682
  _.triggerEl.focus();
421
683
  // mark trigger as no longer expanded
422
684
  _.triggerEl.setAttribute('aria-expanded', 'false');
685
+ } else {
686
+ // If no trigger element, blur any focused element inside the panel
687
+ const activeElement = document.activeElement;
688
+ if (activeElement && _.contains(activeElement)) {
689
+ activeElement.blur();
690
+ }
423
691
  }
424
692
 
425
- // Set aria-hidden to start transition
426
- // The transitionend event handler will add display:none when complete
427
- _.setAttribute('aria-hidden', 'true');
693
+ requestAnimationFrame(() => {
694
+ // Set aria-hidden to start transition
695
+ // The transitionend event handler will add display:none when complete
696
+ _.setAttribute('aria-hidden', 'true');
697
+
698
+ // Emit hide event - cart dialog is now starting to hide
699
+ _.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
428
700
 
429
- // Emit hide event - cart dialog is now starting to hide
430
- _.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
701
+ // Restore body scroll
702
+ _.#restoreScroll();
703
+ });
431
704
  }
432
705
  }
433
706
 
@@ -472,6 +745,15 @@ if (!customElements.get('cart-panel')) {
472
745
  customElements.define('cart-panel', CartPanel);
473
746
  }
474
747
 
748
+ // Make CartItem available globally for Shopify themes
749
+ if (typeof window !== 'undefined') {
750
+ window.CartItem = cartItem.CartItem;
751
+ }
752
+
753
+ Object.defineProperty(exports, 'CartItem', {
754
+ enumerable: true,
755
+ get: function () { return cartItem.CartItem; }
756
+ });
475
757
  exports.CartDialog = CartDialog;
476
758
  exports.CartOverlay = CartOverlay;
477
759
  exports.CartPanel = CartPanel;