@lime-bundles/widget 3.2.1 → 3.2.2

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/dist/index.js CHANGED
@@ -142,6 +142,7 @@ function bindDropdown(select) {
142
142
  let optionEls = [];
143
143
  let instance;
144
144
  function syncFromSelect() {
145
+ trigger.disabled = select.disabled;
145
146
  const opts = readOptions(select);
146
147
  const idx = select.selectedIndex;
147
148
  triggerLabel.textContent = idx >= 0 && opts[idx] ? opts[idx].label : "";
@@ -1051,20 +1052,26 @@ function renderMixMatchBundle(container, bundle, onAddToCart, onCleanup) {
1051
1052
  root.appendChild(pricingSection.el);
1052
1053
  const savingsBar = wc.savingsBar.visible ? renderSavingsBar2() : null;
1053
1054
  if (savingsBar) root.appendChild(savingsBar.el);
1055
+ const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
1056
+ cta.disabled = true;
1057
+ cta.addEventListener("click", () => {
1058
+ if (cta.disabled) return;
1059
+ onAddToCart(buildCartLines(selections, bundle));
1060
+ });
1054
1061
  const modal = renderModal(bundle, eligible, currency, {
1055
1062
  showSearch: wc.showSearch,
1056
1063
  showQtySelector,
1057
1064
  selections,
1058
1065
  onAdd: (product, variant, quantity) => addSelection(product, variant, quantity),
1059
- isComplete: () => selections.length >= requiredQty
1066
+ isComplete: () => selections.length >= requiredQty,
1067
+ getFocusAfterAdd: () => {
1068
+ const empty = slotsContainer.querySelector(
1069
+ ".lb-mix-match__slot--empty"
1070
+ );
1071
+ return empty ?? cta;
1072
+ }
1060
1073
  });
1061
1074
  root.appendChild(modal.el);
1062
- const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);
1063
- cta.disabled = true;
1064
- cta.addEventListener("click", () => {
1065
- if (cta.disabled) return;
1066
- onAddToCart(buildCartLines(selections, bundle));
1067
- });
1068
1075
  root.appendChild(cta);
1069
1076
  root.appendChild(
1070
1077
  el("p", "lb-bundle-error", { "data-error": "", "aria-live": "polite" })
@@ -1196,12 +1203,14 @@ function renderProgress(requiredQty) {
1196
1203
  const wrap = el("div", "lb-mix-match__progress");
1197
1204
  const labels = el("div", "lb-mix-match__progress-labels");
1198
1205
  const count = el("span", "lb-mix-match__progress-count", {
1199
- "data-progress-count": ""
1206
+ "data-progress-count": "",
1207
+ "aria-live": "polite"
1200
1208
  });
1201
1209
  count.textContent = `0 of ${requiredQty} selected`;
1202
1210
  labels.appendChild(count);
1203
1211
  const remaining = el("span", "lb-mix-match__progress-remaining", {
1204
- "data-progress-remaining": ""
1212
+ "data-progress-remaining": "",
1213
+ "aria-live": "polite"
1205
1214
  });
1206
1215
  remaining.textContent = `${requiredQty} more to go`;
1207
1216
  labels.appendChild(remaining);
@@ -1576,12 +1585,12 @@ function renderModal(bundle, eligible, currency, handlers) {
1576
1585
  refreshFromSelections: () => {
1577
1586
  }
1578
1587
  };
1588
+ const optionSelects = [];
1579
1589
  if (availableVariants.length > 1) {
1580
1590
  const optionNames = availableVariants[0].selectedOptions.map(
1581
1591
  (o) => o.name
1582
1592
  );
1583
1593
  const productIdTail = ep.product.id.replace(/^.*\//, "");
1584
- const optionSelects = [];
1585
1594
  const resolveVariant = (values) => availableVariants.find(
1586
1595
  (v) => v.selectedOptions.length === values.length && v.selectedOptions.every((o, i) => o.value === values[i])
1587
1596
  ) ?? null;
@@ -1690,7 +1699,13 @@ function renderModal(bundle, eligible, currency, handlers) {
1690
1699
  productEl.appendChild(info);
1691
1700
  let addBtn = null;
1692
1701
  const refreshAddState = () => {
1693
- if (!addBtn) return;
1702
+ if (!addBtn) {
1703
+ optionSelects.forEach((sel) => {
1704
+ sel.disabled = true;
1705
+ });
1706
+ if (stepper) stepper.setExternallyDisabled(true);
1707
+ return;
1708
+ }
1694
1709
  const { cap } = computeStepperBounds();
1695
1710
  const already = alreadyInBundleFor(
1696
1711
  handlers.selections,
@@ -1709,12 +1724,19 @@ function renderModal(bundle, eligible, currency, handlers) {
1709
1724
  if (productInBundle) {
1710
1725
  addBtn.disabled = true;
1711
1726
  addBtn.textContent = "Added";
1727
+ addBtn.setAttribute("aria-label", `Added ${ep.product.title}`);
1712
1728
  productEl.classList.add("lb-mix-match__modal-product--in-bundle");
1713
1729
  } else {
1714
1730
  addBtn.textContent = "Add";
1731
+ addBtn.setAttribute("aria-label", `Add ${ep.product.title}`);
1715
1732
  productEl.classList.remove("lb-mix-match__modal-product--in-bundle");
1716
1733
  addBtn.disabled = handlers.isComplete() || cap < rule.min;
1717
1734
  }
1735
+ const rowDisabled = addBtn.disabled;
1736
+ optionSelects.forEach((sel) => {
1737
+ sel.disabled = rowDisabled;
1738
+ });
1739
+ if (stepper) stepper.setExternallyDisabled(rowDisabled);
1718
1740
  };
1719
1741
  if (!ep.isOos) {
1720
1742
  const actions = el("div", "lb-mix-match__modal-product-actions");
@@ -1723,12 +1745,15 @@ function renderModal(bundle, eligible, currency, handlers) {
1723
1745
  addBtn.type = "button";
1724
1746
  addBtn.className = "lb-mix-match__modal-add";
1725
1747
  addBtn.textContent = "Add";
1748
+ addBtn.setAttribute("aria-label", `Add ${ep.product.title}`);
1726
1749
  addBtn.addEventListener("click", () => {
1727
1750
  if (!addBtn || addBtn.disabled) return;
1728
1751
  if (handlers.isComplete()) return;
1729
1752
  const qty = stepper ? stepper.value() : rule.min;
1730
1753
  handlers.onAdd(ep.product, currentVariant, qty);
1731
1754
  if (stepper) stepper.reset(rule.min);
1755
+ const nextFocus = handlers.getFocusAfterAdd();
1756
+ if (nextFocus) lastFocused = nextFocus;
1732
1757
  close();
1733
1758
  });
1734
1759
  actions.appendChild(addBtn);
@@ -1824,6 +1849,7 @@ function renderQtyStepper(opts) {
1824
1849
  plus.innerHTML = STEPPER_PLUS_SVG;
1825
1850
  wrap.appendChild(plus);
1826
1851
  let current = clamp2(opts.initial, opts.getBounds());
1852
+ let externallyDisabled = false;
1827
1853
  function clamp2(n, b) {
1828
1854
  return Math.max(b.min, Math.min(b.max, n));
1829
1855
  }
@@ -1831,8 +1857,8 @@ function renderQtyStepper(opts) {
1831
1857
  const b = opts.getBounds();
1832
1858
  current = clamp2(current, b);
1833
1859
  valueEl.textContent = String(current);
1834
- minus.disabled = current <= b.min;
1835
- plus.disabled = current >= b.max;
1860
+ minus.disabled = externallyDisabled || current <= b.min;
1861
+ plus.disabled = externallyDisabled || current >= b.max;
1836
1862
  }
1837
1863
  minus.addEventListener("click", () => {
1838
1864
  const b = opts.getBounds();
@@ -1852,7 +1878,11 @@ function renderQtyStepper(opts) {
1852
1878
  current = clamp2(qty, opts.getBounds());
1853
1879
  paint();
1854
1880
  },
1855
- refresh: paint
1881
+ refresh: paint,
1882
+ setExternallyDisabled(disabled) {
1883
+ externallyDisabled = disabled;
1884
+ paint();
1885
+ }
1856
1886
  };
1857
1887
  }
1858
1888
  function buildEligibleProducts(bundle, oosBehavior) {
@@ -2499,6 +2529,13 @@ var BUNDLE_BASE_CSS = `/* Lime Bundles \u2014 shared base styles for all bundle
2499
2529
  text-decoration: underline;
2500
2530
  }
2501
2531
 
2532
+ a.lb-bundle-product-name:focus-visible {
2533
+ outline: 2px solid var(--lb-primary-color);
2534
+ outline-offset: 0;
2535
+ box-shadow: none;
2536
+ border-radius: 2px;
2537
+ }
2538
+
2502
2539
  .lb-bundle-product-price {
2503
2540
  /* --lb-product-price-display fallback is the "on" branch; Liquid sets 'none' when merchant disables. */
2504
2541
  display: var(--lb-product-price-display, block);
@@ -2947,6 +2984,7 @@ var BUNDLE_FIXED_CSS = `/* Lime Bundles \u2014 Fixed bundle styles */
2947
2984
  .lb-bundle-variant-select:focus-visible {
2948
2985
  outline: 2px solid var(--lb-primary-color);
2949
2986
  outline-offset: 2px;
2987
+ box-shadow: none;
2950
2988
  }
2951
2989
  `;
2952
2990
  var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
@@ -3024,7 +3062,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3024
3062
 
3025
3063
  .lb-mix-match__slot--empty:focus-visible {
3026
3064
  outline: 2px solid var(--lb-primary-color);
3027
- outline-offset: 2px;
3065
+ outline-offset: -2px;
3066
+ box-shadow: none;
3028
3067
  border-radius: 8px;
3029
3068
  }
3030
3069
 
@@ -3091,6 +3130,13 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3091
3130
  text-decoration: underline;
3092
3131
  }
3093
3132
 
3133
+ .lb-mix-match__slot--filled a.lb-mix-match__filled-title:focus-visible {
3134
+ outline: 2px solid var(--lb-primary-color);
3135
+ outline-offset: 2px;
3136
+ box-shadow: none;
3137
+ border-radius: 2px;
3138
+ }
3139
+
3094
3140
  /* .lb-mix-match__filled-variant \u2014 styled jointly with
3095
3141
  .lb-bundle-variant-badge above to keep the variant text consistent
3096
3142
  across mix-match and fixed bundle widgets. */
@@ -3136,7 +3182,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3136
3182
 
3137
3183
  .lb-mix-match__slot-remove:focus-visible {
3138
3184
  outline: 2px solid var(--lb-primary-color);
3139
- outline-offset: 2px;
3185
+ outline-offset: -2px;
3186
+ box-shadow: none;
3140
3187
  }
3141
3188
 
3142
3189
  /* === Modal Overlay === */
@@ -3216,7 +3263,8 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3216
3263
 
3217
3264
  .lb-mix-match__modal-close:focus-visible {
3218
3265
  outline: 2px solid var(--lb-primary-color);
3219
- outline-offset: 2px;
3266
+ outline-offset: 0;
3267
+ box-shadow: none;
3220
3268
  }
3221
3269
 
3222
3270
  /* === Modal Search === */
@@ -3244,9 +3292,10 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3244
3292
  color: color-mix(in srgb, var(--lb-picker-text) 50%, transparent);
3245
3293
  }
3246
3294
 
3247
- .lb-mix-match__modal-search-input:focus {
3248
- outline: none;
3249
- box-shadow: 0 0 0 1px var(--lb-primary-color);
3295
+ .lb-mix-match__modal-search-input:focus-visible {
3296
+ outline: 2px solid var(--lb-primary-color);
3297
+ outline-offset: 2px;
3298
+ box-shadow: none;
3250
3299
  }
3251
3300
 
3252
3301
  .lb-mix-match__modal-search-clear {
@@ -3379,6 +3428,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3379
3428
  .lb-mix-match__variant-select:focus-visible {
3380
3429
  outline: 2px solid var(--lb-primary-color);
3381
3430
  outline-offset: 2px;
3431
+ box-shadow: none;
3382
3432
  }
3383
3433
 
3384
3434
  /* Per-pick quantity stepper inside the picker modal product row.
@@ -3433,13 +3483,10 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3433
3483
  justify-content: center;
3434
3484
  }
3435
3485
 
3436
- .lb-mix-match__qty-stepper-button:hover:not(:disabled) {
3437
- background: color-mix(in srgb, var(--lb-picker-text) 6%, transparent);
3438
- }
3439
-
3440
3486
  .lb-mix-match__qty-stepper-button:focus-visible {
3441
3487
  outline: 2px solid var(--lb-primary-color);
3442
3488
  outline-offset: -2px;
3489
+ box-shadow: none;
3443
3490
  }
3444
3491
 
3445
3492
  .lb-mix-match__qty-stepper-button:disabled {
@@ -3495,6 +3542,7 @@ var BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles \u2014 Mix & Match styles */
3495
3542
  .lb-mix-match__modal-add:focus-visible {
3496
3543
  outline: 2px solid var(--lb-primary-color);
3497
3544
  outline-offset: 2px;
3545
+ box-shadow: none;
3498
3546
  }
3499
3547
 
3500
3548
  .lb-mix-match__modal-add:disabled {
@@ -3612,12 +3660,14 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
3612
3660
  border-color: var(--lb-tier-selected-border-color);
3613
3661
  }
3614
3662
 
3615
- /* Suppress the UA default focus outline so a freshly-clicked selected
3616
- tier doesn't briefly show the 1px focus ring on top of (or in place of)
3617
- the custom selected-state outline below. Keyboard focus is still
3618
- indicated by the :focus-visible rule. */
3663
+ /* Suppress the UA default focus outline + Dawn's focus shadow so a
3664
+ freshly-clicked selected tier doesn't briefly show the 1px focus ring
3665
+ or shadow on top of (or in place of) the custom selected-state
3666
+ outline below. Keyboard focus is still indicated by the
3667
+ :focus-visible rule. */
3619
3668
  .lb-volume__tier:focus {
3620
3669
  outline: none;
3670
+ box-shadow: none;
3621
3671
  }
3622
3672
 
3623
3673
  /* Keyboard focus indicator \u2014 explicitly excludes the selected tier so
@@ -3625,7 +3675,8 @@ var BUNDLE_VOLUME_CSS = `/* Lime Bundles \u2014 Volume / Quantity Breaks styles
3625
3675
  outline property when both states apply at once. */
3626
3676
  .lb-volume__tier:focus-visible:not([aria-checked="true"]) {
3627
3677
  outline: 2px solid var(--lb-primary-color);
3628
- outline-offset: 2px;
3678
+ outline-offset: -2px;
3679
+ box-shadow: none;
3629
3680
  }
3630
3681
 
3631
3682
  .lb-volume__tier[aria-checked="true"] {
@@ -3773,6 +3824,7 @@ var BUNDLE_DROPDOWN_CSS = `/**
3773
3824
  .lb-dropdown-trigger:focus-visible {
3774
3825
  outline: 2px solid var(--lb-primary-color);
3775
3826
  outline-offset: 2px;
3827
+ box-shadow: none;
3776
3828
  }
3777
3829
 
3778
3830
  .lb-dropdown-trigger[aria-expanded="true"] {