@magic-spells/cart-panel 0.1.2 → 0.3.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.
- package/README.md +381 -190
- package/dist/cart-panel.cjs.css +2 -9
- package/dist/cart-panel.cjs.js +215 -80
- package/dist/cart-panel.cjs.js.map +1 -1
- package/dist/cart-panel.css +2 -9
- package/dist/cart-panel.esm.css +2 -9
- package/dist/cart-panel.esm.js +216 -81
- package/dist/cart-panel.esm.js.map +1 -1
- package/dist/cart-panel.js +869 -429
- package/dist/cart-panel.js.map +1 -1
- package/dist/cart-panel.min.css +1 -1
- package/dist/cart-panel.min.js +1 -1
- package/dist/cart-panel.scss +2 -12
- package/package.json +2 -2
- package/src/cart-panel.js +216 -81
- package/src/cart-panel.scss +2 -12
package/src/cart-panel.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import './cart-panel.scss';
|
|
2
|
-
|
|
3
|
-
import { CartItem } from '@magic-spells/cart-item';
|
|
2
|
+
|
|
4
3
|
import '@magic-spells/focus-trap';
|
|
5
4
|
import EventEmitter from '@magic-spells/event-emitter';
|
|
5
|
+
import { CartItem } from '@magic-spells/cart-item';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Custom element that creates an accessible modal cart dialog with focus management
|
|
@@ -10,7 +10,6 @@ import EventEmitter from '@magic-spells/event-emitter';
|
|
|
10
10
|
*/
|
|
11
11
|
class CartDialog extends HTMLElement {
|
|
12
12
|
#handleTransitionEnd;
|
|
13
|
-
#scrollPosition = 0;
|
|
14
13
|
#currentCart = null;
|
|
15
14
|
#eventEmitter;
|
|
16
15
|
#isInitialRender = true;
|
|
@@ -33,31 +32,21 @@ class CartDialog extends HTMLElement {
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
|
-
*
|
|
35
|
+
* Locks body scrolling
|
|
37
36
|
* @private
|
|
38
37
|
*/
|
|
39
38
|
#lockScroll() {
|
|
40
|
-
|
|
41
|
-
// Save current scroll position
|
|
42
|
-
_.#scrollPosition = window.pageYOffset;
|
|
43
|
-
|
|
44
|
-
// Apply fixed position to body
|
|
39
|
+
// Apply overflow hidden to body
|
|
45
40
|
document.body.classList.add('overflow-hidden');
|
|
46
|
-
document.body.style.top = `-${_.#scrollPosition}px`;
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
/**
|
|
50
|
-
* Restores
|
|
44
|
+
* Restores body scrolling when cart dialog is closed
|
|
51
45
|
* @private
|
|
52
46
|
*/
|
|
53
47
|
#restoreScroll() {
|
|
54
|
-
|
|
55
|
-
// Remove fixed positioning
|
|
48
|
+
// Remove overflow hidden from body
|
|
56
49
|
document.body.classList.remove('overflow-hidden');
|
|
57
|
-
document.body.style.removeProperty('top');
|
|
58
|
-
|
|
59
|
-
// Restore scroll position
|
|
60
|
-
window.scrollTo(0, _.#scrollPosition);
|
|
61
50
|
}
|
|
62
51
|
|
|
63
52
|
/**
|
|
@@ -98,7 +87,18 @@ class CartDialog extends HTMLElement {
|
|
|
98
87
|
return;
|
|
99
88
|
}
|
|
100
89
|
|
|
101
|
-
|
|
90
|
+
// Check if focus-trap already exists, if not create one
|
|
91
|
+
_.focusTrap = _.contentPanel.querySelector('focus-trap');
|
|
92
|
+
if (!_.focusTrap) {
|
|
93
|
+
_.focusTrap = document.createElement('focus-trap');
|
|
94
|
+
|
|
95
|
+
// Move all existing cart-panel content into the focus trap
|
|
96
|
+
const existingContent = Array.from(_.contentPanel.childNodes);
|
|
97
|
+
existingContent.forEach((child) => _.focusTrap.appendChild(child));
|
|
98
|
+
|
|
99
|
+
// Insert focus trap inside the cart-panel
|
|
100
|
+
_.contentPanel.appendChild(_.focusTrap);
|
|
101
|
+
}
|
|
102
102
|
|
|
103
103
|
// Ensure we have labelledby and describedby references
|
|
104
104
|
if (!_.getAttribute('aria-labelledby')) {
|
|
@@ -111,20 +111,15 @@ class CartDialog extends HTMLElement {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// Insert focus trap before the cart-panel
|
|
115
|
-
_.contentPanel.parentNode.insertBefore(_.focusTrap, _.contentPanel);
|
|
116
|
-
// Move cart-panel inside the focus trap
|
|
117
|
-
_.focusTrap.appendChild(_.contentPanel);
|
|
118
|
-
|
|
119
|
-
// Setup the trap - this will add focus-trap-start/end elements around the content
|
|
120
|
-
_.focusTrap.setupTrap();
|
|
121
|
-
|
|
122
114
|
// Add modal overlay if it doesn't already exist
|
|
123
115
|
if (!_.querySelector('cart-overlay')) {
|
|
124
116
|
_.prepend(document.createElement('cart-overlay'));
|
|
125
117
|
}
|
|
126
118
|
_.#attachListeners();
|
|
127
119
|
_.#bindKeyboard();
|
|
120
|
+
|
|
121
|
+
// Load cart data immediately after component initialization
|
|
122
|
+
_.refreshCart();
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
/**
|
|
@@ -157,6 +152,14 @@ class CartDialog extends HTMLElement {
|
|
|
157
152
|
*/
|
|
158
153
|
#emit(eventName, data = null) {
|
|
159
154
|
this.#eventEmitter.emit(eventName, data);
|
|
155
|
+
|
|
156
|
+
// Also emit as native DOM events for better compatibility
|
|
157
|
+
this.dispatchEvent(
|
|
158
|
+
new CustomEvent(eventName, {
|
|
159
|
+
detail: data,
|
|
160
|
+
bubbles: true,
|
|
161
|
+
})
|
|
162
|
+
);
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/**
|
|
@@ -238,11 +241,12 @@ class CartDialog extends HTMLElement {
|
|
|
238
241
|
// Success - let smart comparison handle the removal animation
|
|
239
242
|
this.#currentCart = updatedCart;
|
|
240
243
|
this.#renderCartItems(updatedCart);
|
|
241
|
-
this.#
|
|
244
|
+
this.#renderCartPanel(updatedCart);
|
|
242
245
|
|
|
243
246
|
// Emit cart updated and data changed events
|
|
244
|
-
this.#
|
|
245
|
-
this.#emit('cart-dialog:
|
|
247
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
|
|
248
|
+
this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
|
|
249
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
246
250
|
} else {
|
|
247
251
|
// Error - reset to ready state
|
|
248
252
|
element.setState('ready');
|
|
@@ -273,12 +277,12 @@ class CartDialog extends HTMLElement {
|
|
|
273
277
|
// Success - update cart data and refresh items
|
|
274
278
|
this.#currentCart = updatedCart;
|
|
275
279
|
this.#renderCartItems(updatedCart);
|
|
276
|
-
this.#
|
|
277
|
-
element.setState('ready');
|
|
280
|
+
this.#renderCartPanel(updatedCart);
|
|
278
281
|
|
|
279
282
|
// Emit cart updated and data changed events
|
|
280
|
-
this.#
|
|
281
|
-
this.#emit('cart-dialog:
|
|
283
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(updatedCart);
|
|
284
|
+
this.#emit('cart-dialog:updated', { cart: cartWithCalculatedFields });
|
|
285
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
282
286
|
} else {
|
|
283
287
|
// Error - reset to ready state
|
|
284
288
|
element.setState('ready');
|
|
@@ -292,11 +296,52 @@ class CartDialog extends HTMLElement {
|
|
|
292
296
|
});
|
|
293
297
|
}
|
|
294
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Update cart count elements across the site
|
|
301
|
+
* @private
|
|
302
|
+
*/
|
|
303
|
+
#renderCartCount(cartData) {
|
|
304
|
+
if (!cartData) return;
|
|
305
|
+
|
|
306
|
+
// Calculate visible item count (excluding _hide_in_cart items)
|
|
307
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
308
|
+
const visibleItemCount = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
309
|
+
|
|
310
|
+
// Update all cart count elements across the site
|
|
311
|
+
const cartCountElements = document.querySelectorAll('[data-content-cart-count]');
|
|
312
|
+
cartCountElements.forEach((element) => {
|
|
313
|
+
element.textContent = visibleItemCount;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Update cart subtotal elements across the site
|
|
319
|
+
* @private
|
|
320
|
+
*/
|
|
321
|
+
#renderCartSubtotal(cartData) {
|
|
322
|
+
if (!cartData) return;
|
|
323
|
+
|
|
324
|
+
// Calculate subtotal from all items except those marked to ignore pricing
|
|
325
|
+
const pricedItems = cartData.items.filter((item) => {
|
|
326
|
+
const ignorePrice = item.properties?._ignore_price_in_subtotal;
|
|
327
|
+
return !ignorePrice;
|
|
328
|
+
});
|
|
329
|
+
const subtotal = pricedItems.reduce((total, item) => total + (item.line_price || 0), 0);
|
|
330
|
+
|
|
331
|
+
// Update all cart subtotal elements across the site
|
|
332
|
+
const cartSubtotalElements = document.querySelectorAll('[data-content-cart-subtotal]');
|
|
333
|
+
cartSubtotalElements.forEach((element) => {
|
|
334
|
+
// Format as currency (assuming cents, convert to dollars)
|
|
335
|
+
const formatted = (subtotal / 100).toFixed(2);
|
|
336
|
+
element.textContent = `$${formatted}`;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
295
340
|
/**
|
|
296
341
|
* Update cart items display based on cart data
|
|
297
342
|
* @private
|
|
298
343
|
*/
|
|
299
|
-
#
|
|
344
|
+
#renderCartPanel(cart = null) {
|
|
300
345
|
const cartData = cart || this.#currentCart;
|
|
301
346
|
if (!cartData) return;
|
|
302
347
|
|
|
@@ -312,14 +357,22 @@ class CartDialog extends HTMLElement {
|
|
|
312
357
|
return;
|
|
313
358
|
}
|
|
314
359
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
360
|
+
// Check visible item count for showing/hiding sections
|
|
361
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
362
|
+
const hasVisibleItems = visibleItems.length > 0;
|
|
363
|
+
|
|
364
|
+
// Show/hide sections based on visible item count
|
|
365
|
+
if (hasVisibleItems) {
|
|
366
|
+
hasItemsSection.style.display = '';
|
|
318
367
|
emptySection.style.display = 'none';
|
|
319
368
|
} else {
|
|
320
369
|
hasItemsSection.style.display = 'none';
|
|
321
|
-
emptySection.style.display = '
|
|
370
|
+
emptySection.style.display = '';
|
|
322
371
|
}
|
|
372
|
+
|
|
373
|
+
// Update cart count and subtotal across the site
|
|
374
|
+
this.#renderCartCount(cartData);
|
|
375
|
+
this.#renderCartSubtotal(cartData);
|
|
323
376
|
}
|
|
324
377
|
|
|
325
378
|
/**
|
|
@@ -371,20 +424,37 @@ class CartDialog extends HTMLElement {
|
|
|
371
424
|
|
|
372
425
|
/**
|
|
373
426
|
* Refresh cart data from server and update components
|
|
427
|
+
* @param {Object} [cartObj=null] - Optional cart object to use instead of fetching
|
|
374
428
|
* @returns {Promise<Object>} Cart data object
|
|
375
429
|
*/
|
|
376
|
-
refreshCart() {
|
|
377
|
-
|
|
430
|
+
refreshCart(cartObj = null) {
|
|
431
|
+
// If cart object is provided, use it directly
|
|
432
|
+
if (cartObj && !cartObj.error) {
|
|
433
|
+
// console.log('Using provided cart data:', cartObj);
|
|
434
|
+
this.#currentCart = cartObj;
|
|
435
|
+
this.#renderCartItems(cartObj);
|
|
436
|
+
this.#renderCartPanel(cartObj);
|
|
437
|
+
|
|
438
|
+
// Emit cart refreshed and data changed events
|
|
439
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(cartObj);
|
|
440
|
+
this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
|
|
441
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
442
|
+
|
|
443
|
+
return Promise.resolve(cartObj);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Otherwise fetch from server
|
|
378
447
|
return this.getCart().then((cartData) => {
|
|
379
|
-
console.log('Cart data received:', cartData);
|
|
448
|
+
// console.log('Cart data received:', cartData);
|
|
380
449
|
if (cartData && !cartData.error) {
|
|
381
450
|
this.#currentCart = cartData;
|
|
382
451
|
this.#renderCartItems(cartData);
|
|
383
|
-
this.#
|
|
452
|
+
this.#renderCartPanel(cartData);
|
|
384
453
|
|
|
385
454
|
// Emit cart refreshed and data changed events
|
|
386
|
-
this.#
|
|
387
|
-
this.#emit('cart-dialog:
|
|
455
|
+
const cartWithCalculatedFields = this.#addCalculatedFields(cartData);
|
|
456
|
+
this.#emit('cart-dialog:refreshed', { cart: cartWithCalculatedFields });
|
|
457
|
+
this.#emit('cart-dialog:data-changed', cartWithCalculatedFields);
|
|
388
458
|
} else {
|
|
389
459
|
console.warn('Cart data has error or is null:', cartData);
|
|
390
460
|
}
|
|
@@ -398,28 +468,40 @@ class CartDialog extends HTMLElement {
|
|
|
398
468
|
*/
|
|
399
469
|
#removeItemsFromDOM(itemsContainer, newKeysSet) {
|
|
400
470
|
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
401
|
-
const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
|
|
402
471
|
|
|
403
|
-
|
|
404
|
-
`Removing ${itemsToRemove.length} items:`,
|
|
405
|
-
itemsToRemove.map((item) => item.getAttribute('key'))
|
|
406
|
-
);
|
|
472
|
+
const itemsToRemove = currentItems.filter((item) => !newKeysSet.has(item.getAttribute('key')));
|
|
407
473
|
|
|
408
474
|
itemsToRemove.forEach((item) => {
|
|
475
|
+
console.log('destroy yourself', item);
|
|
409
476
|
item.destroyYourself();
|
|
410
477
|
});
|
|
411
478
|
}
|
|
412
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Update existing cart-item elements with fresh cart data
|
|
482
|
+
* @private
|
|
483
|
+
*/
|
|
484
|
+
#updateItemsInDOM(itemsContainer, cartData) {
|
|
485
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
486
|
+
const existingItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
487
|
+
|
|
488
|
+
existingItems.forEach((cartItemEl) => {
|
|
489
|
+
const key = cartItemEl.getAttribute('key');
|
|
490
|
+
const updatedItemData = visibleItems.find((item) => (item.key || item.id) === key);
|
|
491
|
+
|
|
492
|
+
if (updatedItemData) {
|
|
493
|
+
// Update cart-item with fresh data and full cart context
|
|
494
|
+
// The cart-item will handle HTML comparison and only re-render if needed
|
|
495
|
+
cartItemEl.setData(updatedItemData, cartData);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
413
500
|
/**
|
|
414
501
|
* Add new items to DOM with animation delay
|
|
415
502
|
* @private
|
|
416
503
|
*/
|
|
417
504
|
#addItemsToDOM(itemsContainer, itemsToAdd, newKeys) {
|
|
418
|
-
console.log(
|
|
419
|
-
`Adding ${itemsToAdd.length} items:`,
|
|
420
|
-
itemsToAdd.map((item) => item.key || item.id)
|
|
421
|
-
);
|
|
422
|
-
|
|
423
505
|
// Delay adding new items by 300ms to let cart slide open first
|
|
424
506
|
setTimeout(() => {
|
|
425
507
|
itemsToAdd.forEach((itemData) => {
|
|
@@ -452,6 +534,48 @@ class CartDialog extends HTMLElement {
|
|
|
452
534
|
}, 100);
|
|
453
535
|
}
|
|
454
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Filter cart items to exclude those with _hide_in_cart property
|
|
539
|
+
* @private
|
|
540
|
+
*/
|
|
541
|
+
#getVisibleCartItems(cartData) {
|
|
542
|
+
if (!cartData || !cartData.items) return [];
|
|
543
|
+
return cartData.items.filter((item) => {
|
|
544
|
+
// Check for _hide_in_cart in various possible locations
|
|
545
|
+
const hidden = item.properties?._hide_in_cart;
|
|
546
|
+
|
|
547
|
+
return !hidden;
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Add calculated fields to cart object for events
|
|
553
|
+
* @private
|
|
554
|
+
*/
|
|
555
|
+
#addCalculatedFields(cartData) {
|
|
556
|
+
if (!cartData) return cartData;
|
|
557
|
+
|
|
558
|
+
// For display counts: use visible items (excludes _hide_in_cart)
|
|
559
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
560
|
+
const calculated_count = visibleItems.reduce((total, item) => total + item.quantity, 0);
|
|
561
|
+
|
|
562
|
+
// For pricing: use all items except those marked to ignore pricing
|
|
563
|
+
const pricedItems = cartData.items.filter((item) => {
|
|
564
|
+
const ignorePrice = item.properties?._ignore_price_in_subtotal;
|
|
565
|
+
return !ignorePrice;
|
|
566
|
+
});
|
|
567
|
+
const calculated_subtotal = pricedItems.reduce(
|
|
568
|
+
(total, item) => total + (item.line_price || 0),
|
|
569
|
+
0
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
...cartData,
|
|
574
|
+
calculated_count,
|
|
575
|
+
calculated_subtotal,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
455
579
|
/**
|
|
456
580
|
* Render cart items from Shopify cart data with smart comparison
|
|
457
581
|
* @private
|
|
@@ -468,53 +592,54 @@ class CartDialog extends HTMLElement {
|
|
|
468
592
|
return;
|
|
469
593
|
}
|
|
470
594
|
|
|
595
|
+
// Filter out items with _hide_in_cart property
|
|
596
|
+
const visibleItems = this.#getVisibleCartItems(cartData);
|
|
597
|
+
|
|
471
598
|
// Handle initial render - load all items without animation
|
|
472
599
|
if (this.#isInitialRender) {
|
|
473
|
-
console.log('Initial cart render:',
|
|
600
|
+
// console.log('Initial cart render:', visibleItems.length, 'visible items');
|
|
474
601
|
|
|
475
602
|
// Clear existing items
|
|
476
603
|
itemsContainer.innerHTML = '';
|
|
477
604
|
|
|
478
605
|
// Create cart-item elements without animation
|
|
479
|
-
|
|
606
|
+
visibleItems.forEach((itemData) => {
|
|
480
607
|
const cartItem = new CartItem(itemData); // No animation
|
|
481
608
|
itemsContainer.appendChild(cartItem);
|
|
482
609
|
});
|
|
483
610
|
|
|
484
611
|
this.#isInitialRender = false;
|
|
485
|
-
|
|
612
|
+
|
|
486
613
|
return;
|
|
487
614
|
}
|
|
488
615
|
|
|
489
|
-
console.log('Smart rendering cart items:', cartData.items.length, 'items');
|
|
490
|
-
|
|
491
616
|
// Get current DOM items and their keys
|
|
492
617
|
const currentItems = Array.from(itemsContainer.querySelectorAll('cart-item'));
|
|
493
618
|
const currentKeys = new Set(currentItems.map((item) => item.getAttribute('key')));
|
|
494
619
|
|
|
495
|
-
// Get new cart data keys in order
|
|
496
|
-
const newKeys =
|
|
620
|
+
// Get new cart data keys in order (only visible items)
|
|
621
|
+
const newKeys = visibleItems.map((item) => item.key || item.id);
|
|
497
622
|
const newKeysSet = new Set(newKeys);
|
|
498
623
|
|
|
499
624
|
// Step 1: Remove items that are no longer in cart data
|
|
500
625
|
this.#removeItemsFromDOM(itemsContainer, newKeysSet);
|
|
501
626
|
|
|
502
|
-
// Step 2:
|
|
503
|
-
|
|
627
|
+
// Step 2: Update existing items with fresh data (handles templates, bundles, etc.)
|
|
628
|
+
this.#updateItemsInDOM(itemsContainer, cartData);
|
|
629
|
+
|
|
630
|
+
// Step 3: Add new items that weren't in DOM (with animation delay)
|
|
631
|
+
const itemsToAdd = visibleItems.filter(
|
|
504
632
|
(itemData) => !currentKeys.has(itemData.key || itemData.id)
|
|
505
633
|
);
|
|
506
|
-
|
|
507
634
|
this.#addItemsToDOM(itemsContainer, itemsToAdd, newKeys);
|
|
508
|
-
|
|
509
|
-
console.log('Smart rendering complete, container children:', itemsContainer.children.length);
|
|
510
635
|
}
|
|
511
636
|
|
|
512
637
|
/**
|
|
513
638
|
* Set the template function for cart items
|
|
514
639
|
* @param {Function} templateFn - Function that takes item data and returns HTML string
|
|
515
640
|
*/
|
|
516
|
-
setCartItemTemplate(templateFn) {
|
|
517
|
-
CartItem.setTemplate(templateFn);
|
|
641
|
+
setCartItemTemplate(templateName, templateFn) {
|
|
642
|
+
CartItem.setTemplate(templateName, templateFn);
|
|
518
643
|
}
|
|
519
644
|
|
|
520
645
|
/**
|
|
@@ -530,10 +655,13 @@ class CartDialog extends HTMLElement {
|
|
|
530
655
|
* @param {HTMLElement} [triggerEl=null] - The element that triggered the cart dialog
|
|
531
656
|
* @fires CartDialog#show - Fired when the cart dialog has been shown
|
|
532
657
|
*/
|
|
533
|
-
show(triggerEl = null) {
|
|
658
|
+
show(triggerEl = null, cartObj) {
|
|
534
659
|
const _ = this;
|
|
535
660
|
_.triggerEl = triggerEl || false;
|
|
536
661
|
|
|
662
|
+
// Lock body scrolling
|
|
663
|
+
_.#lockScroll();
|
|
664
|
+
|
|
537
665
|
// Remove the hidden class first to ensure content is rendered
|
|
538
666
|
_.contentPanel.classList.remove('hidden');
|
|
539
667
|
|
|
@@ -541,17 +669,16 @@ class CartDialog extends HTMLElement {
|
|
|
541
669
|
requestAnimationFrame(() => {
|
|
542
670
|
// Update ARIA states
|
|
543
671
|
_.setAttribute('aria-hidden', 'false');
|
|
672
|
+
|
|
544
673
|
if (_.triggerEl) {
|
|
545
674
|
_.triggerEl.setAttribute('aria-expanded', 'true');
|
|
546
675
|
}
|
|
547
676
|
|
|
548
|
-
// Lock body scrolling and save scroll position
|
|
549
|
-
_.#lockScroll();
|
|
550
|
-
|
|
551
677
|
// Focus management
|
|
552
678
|
const firstFocusable = _.querySelector(
|
|
553
679
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
554
680
|
);
|
|
681
|
+
|
|
555
682
|
if (firstFocusable) {
|
|
556
683
|
requestAnimationFrame(() => {
|
|
557
684
|
firstFocusable.focus();
|
|
@@ -559,7 +686,7 @@ class CartDialog extends HTMLElement {
|
|
|
559
686
|
}
|
|
560
687
|
|
|
561
688
|
// Refresh cart data when showing
|
|
562
|
-
_.refreshCart();
|
|
689
|
+
_.refreshCart(cartObj);
|
|
563
690
|
|
|
564
691
|
// Emit show event - cart dialog is now visible
|
|
565
692
|
_.#emit('cart-dialog:show', { triggerElement: _.triggerEl });
|
|
@@ -574,23 +701,31 @@ class CartDialog extends HTMLElement {
|
|
|
574
701
|
hide() {
|
|
575
702
|
const _ = this;
|
|
576
703
|
|
|
577
|
-
// Restore body scroll and scroll position
|
|
578
|
-
_.#restoreScroll();
|
|
579
|
-
|
|
580
704
|
// Update ARIA states
|
|
581
705
|
if (_.triggerEl) {
|
|
582
706
|
// remove focus from modal panel first
|
|
583
707
|
_.triggerEl.focus();
|
|
584
708
|
// mark trigger as no longer expanded
|
|
585
709
|
_.triggerEl.setAttribute('aria-expanded', 'false');
|
|
710
|
+
} else {
|
|
711
|
+
// If no trigger element, blur any focused element inside the panel
|
|
712
|
+
const activeElement = document.activeElement;
|
|
713
|
+
if (activeElement && _.contains(activeElement)) {
|
|
714
|
+
activeElement.blur();
|
|
715
|
+
}
|
|
586
716
|
}
|
|
587
717
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
718
|
+
requestAnimationFrame(() => {
|
|
719
|
+
// Set aria-hidden to start transition
|
|
720
|
+
// The transitionend event handler will add display:none when complete
|
|
721
|
+
_.setAttribute('aria-hidden', 'true');
|
|
591
722
|
|
|
592
|
-
|
|
593
|
-
|
|
723
|
+
// Emit hide event - cart dialog is now starting to hide
|
|
724
|
+
_.#emit('cart-dialog:hide', { triggerElement: _.triggerEl });
|
|
725
|
+
|
|
726
|
+
// Restore body scroll
|
|
727
|
+
_.#restoreScroll();
|
|
728
|
+
});
|
|
594
729
|
}
|
|
595
730
|
}
|
|
596
731
|
|
package/src/cart-panel.scss
CHANGED
|
@@ -85,8 +85,7 @@ cart-panel {
|
|
|
85
85
|
top: 0;
|
|
86
86
|
right: 0;
|
|
87
87
|
width: var(--cart-panel-width);
|
|
88
|
-
height:
|
|
89
|
-
opacity: 0;
|
|
88
|
+
height: 100dvh;
|
|
90
89
|
transform: translateX(100%);
|
|
91
90
|
pointer-events: none;
|
|
92
91
|
z-index: var(--cart-panel-z-index);
|
|
@@ -94,9 +93,7 @@ cart-panel {
|
|
|
94
93
|
box-shadow: var(--cart-panel-shadow);
|
|
95
94
|
border-radius: var(--cart-panel-border-radius);
|
|
96
95
|
overflow: hidden;
|
|
97
|
-
transition:
|
|
98
|
-
opacity var(--cart-transition-duration) var(--cart-transition-timing),
|
|
99
|
-
transform var(--cart-transition-duration) var(--cart-transition-timing);
|
|
96
|
+
transition: transform var(--cart-transition-duration) var(--cart-transition-timing);
|
|
100
97
|
|
|
101
98
|
// When explicitly hidden, remove from layout
|
|
102
99
|
&.hidden {
|
|
@@ -107,11 +104,4 @@ cart-panel {
|
|
|
107
104
|
// Body scroll lock when cart is open
|
|
108
105
|
body.overflow-hidden {
|
|
109
106
|
overflow: hidden;
|
|
110
|
-
position: fixed;
|
|
111
|
-
width: 100%;
|
|
112
|
-
height: 100%;
|
|
113
|
-
left: 0;
|
|
114
|
-
right: 0;
|
|
115
|
-
margin: 0;
|
|
116
|
-
// The top property will be set dynamically by the component
|
|
117
107
|
}
|