← back to Dw Validator Debug TK11314
improve Shopify purchase and collection UX
a6461a60846ee1689e81b43754feaf9e57061642 · 2026-08-27 19:49:24 -0700 · Steve
Files touched
M shopify/sections/collection-toolbar.liquidA shopify/tests/theme-ux.test.jsM shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-restyle.cssM shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-size-pills.jsM shopify/theme-LIVE-pull-20260728-colorbar/layout/theme.liquid
Diff
commit a6461a60846ee1689e81b43754feaf9e57061642
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Aug 27 19:49:24 2026 -0700
improve Shopify purchase and collection UX
---
shopify/sections/collection-toolbar.liquid | 19 ++--
shopify/tests/theme-ux.test.js | 75 +++++++++++++++
.../assets/dw-pdp-restyle.css | 70 +++++++++++++-
.../assets/dw-pdp-size-pills.js | 103 ++++++++++++++++++++-
.../layout/theme.liquid | 6 --
5 files changed, 248 insertions(+), 25 deletions(-)
diff --git a/shopify/sections/collection-toolbar.liquid b/shopify/sections/collection-toolbar.liquid
index 7cb2e83e..329d043d 100644
--- a/shopify/sections/collection-toolbar.liquid
+++ b/shopify/sections/collection-toolbar.liquid
@@ -169,9 +169,8 @@
margin-top: 8px;
}
- .density-control {
- display: none;
- }
+ .density-control { flex: 1 1 100%; }
+ .density-control input[type="range"] { flex: 1 1 auto; }
}
</style>
@@ -218,27 +217,27 @@
// Load saved state
try {
const savedSort = localStorage.getItem(STORAGE_KEY_SORT);
- if (savedSort) sortSelect.value = savedSort;
+ if (savedSort && sortSelect) sortSelect.value = savedSort;
const savedDensity = localStorage.getItem(STORAGE_KEY_DENSITY);
- if (savedDensity) {
+ if (savedDensity && densitySlider && densityLabel) {
densitySlider.value = savedDensity;
densityLabel.textContent = savedDensity;
updateGridDensity(parseInt(savedDensity));
}
const savedSearch = localStorage.getItem(STORAGE_KEY_SEARCH);
- if (savedSearch) searchInput.value = savedSearch;
+ if (savedSearch && searchInput) searchInput.value = savedSearch;
} catch (e) {}
// Sort change
- sortSelect.addEventListener('change', function() {
+ if (sortSelect) sortSelect.addEventListener('change', function() {
try { localStorage.setItem(STORAGE_KEY_SORT, this.value); } catch (e) {}
applySort(this.value);
});
// Density slider
- densitySlider.addEventListener('input', function() {
+ if (densitySlider) densitySlider.addEventListener('input', function() {
const cols = parseInt(this.value);
densityLabel.textContent = cols;
try { localStorage.setItem(STORAGE_KEY_DENSITY, cols); } catch (e) {}
@@ -246,7 +245,7 @@
});
// Search
- searchInput.addEventListener('input', debounce(function() {
+ if (searchInput) searchInput.addEventListener('input', debounce(function() {
try { localStorage.setItem(STORAGE_KEY_SEARCH, this.value); } catch (e) {}
applySearch(this.value);
}, 300));
@@ -295,7 +294,7 @@
// Initialize grid on page load
window.addEventListener('load', function() {
- updateGridDensity(parseInt(densitySlider.value));
+ if (densitySlider) updateGridDensity(parseInt(densitySlider.value));
});
})();
</script>
diff --git a/shopify/tests/theme-ux.test.js b/shopify/tests/theme-ux.test.js
new file mode 100644
index 00000000..b7b0977b
--- /dev/null
+++ b/shopify/tests/theme-ux.test.js
@@ -0,0 +1,75 @@
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+const vm = require('node:vm');
+
+const theme = path.resolve(__dirname, '../theme-LIVE-pull-20260728-colorbar');
+
+function read(relativePath) {
+ return fs.readFileSync(path.join(theme, relativePath), 'utf8');
+}
+
+function loadPurchaseOptions() {
+ const source = read('assets/dw-pdp-size-pills.js');
+ const sandbox = {
+ window: { addEventListener() {} },
+ document: { readyState: 'loading', addEventListener() {} },
+ Event: function Event() {}
+ };
+ vm.runInNewContext(source, sandbox);
+ return sandbox.window.DWPdpPurchaseOptions;
+}
+
+test('theme loads the grid controller only once', () => {
+ const layout = read('layout/theme.liquid');
+ assert.equal((layout.match(/dw-grid-control\.js/g) || []).length, 1);
+});
+
+test('purchase choices classify samples and full-size formats', () => {
+ const options = loadPurchaseOptions();
+ assert.equal(options.optionKind('Sample - $4.25', 'ABC-Sample'), 'sample');
+ assert.equal(options.optionKind('Sold Per Roll - $205.00', 'ABC'), 'roll');
+ assert.equal(options.optionKind('Bolt - $450.00', 'ABC'), 'bolt');
+ assert.equal(options.optionKind('Sold by the Yard - $89.00', 'ABC'), 'yardage');
+});
+
+test('purchase choice copy separates title, detail, and price', () => {
+ const options = loadPurchaseOptions();
+ const copy = options.optionCopy({
+ textContent: 'Sold Per Roll - 20.5 in wide x 11 yds - $205.00',
+ value: 'roll',
+ getAttribute() { return 'ABC'; }
+ });
+ assert.equal(copy.title, 'Buy a roll');
+ assert.match(copy.detail, /Sold Per Roll/);
+ assert.equal(copy.price, '$205.00');
+});
+
+test('mobile purchase bar delegates to the native cart control', () => {
+ const source = read('assets/dw-pdp-size-pills.js');
+ const css = read('assets/dw-pdp-restyle.css');
+ assert.match(source, /currentAction\.click\(\)/);
+ assert.match(source, /isSample && sampleButton \? sampleButton : nativeButton/);
+ assert.match(source, /isSample \? 'Order sample'/);
+ assert.match(css, /\.dw-mobile-purchase\s*\{/);
+ assert.match(css, /position:\s*fixed/);
+ assert.match(css, /env\(safe-area-inset-bottom\)/);
+});
+
+test('search accessibility repair targets missing translation labels', () => {
+ const source = read('assets/dw-pdp-size-pills.js');
+ assert.match(source, /Translation missing/);
+ assert.match(source, /Search products/);
+});
+
+test('collection toolbar retains complete sort and persistent density controls', () => {
+ const toolbar = fs.readFileSync(path.resolve(__dirname, '../sections/collection-toolbar.liquid'), 'utf8');
+ for (const value of ['newest', 'color', 'style', 'sku', 'title', 'price-asc', 'price-desc']) {
+ assert.match(toolbar, new RegExp(`value="${value}"`));
+ }
+ assert.match(toolbar, /type="range"[^>]+id="densitySlider"/);
+ assert.match(toolbar, /dw_collection_sort/);
+ assert.match(toolbar, /dw_collection_density/);
+ assert.match(toolbar, /if \(searchInput\) searchInput\.addEventListener/);
+});
diff --git a/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-restyle.css b/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-restyle.css
index 3ca14bec..8fe79478 100644
--- a/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-restyle.css
+++ b/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-restyle.css
@@ -42,6 +42,14 @@
margin: 6px 0 18px;
}
+.template-product .dw-purchase-options__heading {
+ flex: 1 0 100%;
+ margin: 0 0 2px;
+ font: 600 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ letter-spacing: .01em;
+ text-transform: none;
+}
+
.template-product .dw-size-pill {
font-family: var(--dw-pill-font);
font-weight: 700;
@@ -50,15 +58,41 @@
text-transform: uppercase;
line-height: 1.1;
cursor: pointer;
- padding: 14px 22px;
- min-height: 46px;
- border-radius: var(--dw-pill-radius);
+ padding: 14px 16px;
+ min-height: 92px;
+ border-radius: 12px;
border: 1.5px solid #111;
background: #fff;
color: #111;
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
-webkit-appearance: none;
appearance: none;
+ flex: 1 1 calc(50% - 5px);
+ text-align: left;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-content: center;
+ gap: 4px 12px;
+}
+
+.template-product .dw-size-pill__title {
+ grid-column: 1;
+ font-size: 13px;
+}
+
+.template-product .dw-size-pill__detail {
+ grid-column: 1 / -1;
+ font: 400 12px/1.35 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ letter-spacing: 0;
+ text-transform: none;
+ opacity: .72;
+}
+
+.template-product .dw-size-pill__price {
+ grid-column: 2;
+ grid-row: 1;
+ font-size: 14px;
+ white-space: nowrap;
}
.template-product .dw-size-pill:hover {
@@ -159,6 +193,34 @@
}
}
+.dw-mobile-purchase { display: none; }
+
+@media screen and (max-width: 720px) {
+ body.template-product { padding-bottom: 84px; }
+ .template-product .dw-size-pill { flex-basis: 100%; }
+ .dw-mobile-purchase {
+ position: fixed;
+ z-index: 2147483000;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ padding: 10px max(14px, env(safe-area-inset-right)) calc(10px + env(safe-area-inset-bottom)) max(14px, env(safe-area-inset-left));
+ border-top: 1px solid rgba(0,0,0,.14);
+ background: rgba(255,255,255,.97);
+ box-shadow: 0 -8px 24px rgba(0,0,0,.08);
+ backdrop-filter: blur(12px);
+ }
+ .dw-mobile-purchase__summary { min-width: 0; display: grid; gap: 2px; }
+ .dw-mobile-purchase__summary span { font: 500 10px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; text-transform: uppercase; letter-spacing: .08em; opacity: .62; }
+ .dw-mobile-purchase__summary strong { overflow: hidden; font: 700 13px/1.25 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; text-overflow: ellipsis; white-space: nowrap; }
+ .dw-mobile-purchase__button { flex: 0 0 auto; min-height: 48px; padding: 0 20px; border: 1px solid #111; border-radius: 99px; background: #111; color: #fff; font: 700 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; text-transform: uppercase; letter-spacing: .04em; }
+ .dw-mobile-purchase__button:disabled { opacity: .45; }
+}
+
/* ── DW 2026-06-25: keep the printable Tear Sheet (.dw-spec-page) OUT of the
on-screen PDP. It was rendering inline — a duplicate <h1> product title +
the Purchasing-Agency address block. Off-screen (NOT display:none) so the
@@ -185,6 +247,6 @@
/* DW 2026-06-25: Download-PDF pill joins the size-pill row next to Sample; min L/R padding, aligned. */
.template-product .dw-size-pills{align-items:center;}
-.template-product .dw-size-pill{padding-left:12px !important;padding-right:12px !important;}
+.template-product .dw-size-pill{padding-left:16px !important;padding-right:16px !important;}
.template-product .dw-size-pills .spec-sheet-controls{margin:0 !important;transform:none !important;}
.template-product .dw-size-pills .spec-sheet-btn.pdf-download-btn{width:auto !important;max-width:none !important;min-height:46px;padding:12px !important;border-radius:var(--dw-pill-radius) !important;background:#fff;color:#111;border:1.5px solid #111;font:700 12px/1.1 var(--dw-pill-font);letter-spacing:.02em;text-transform:uppercase;}
diff --git a/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-size-pills.js b/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-size-pills.js
index c85488dd..5cc2c6e6 100644
--- a/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-size-pills.js
+++ b/shopify/theme-LIVE-pull-20260728-colorbar/assets/dw-pdp-size-pills.js
@@ -5,7 +5,7 @@
* <select class="single-option-selector"> that the theme's variant /
* cart / cowlendar / MOQ JS reads. We:
* 1. keep the <select> in the DOM (visually hidden via dw-pdp-restyle.css),
- * 2. render one clickable pill <button> per <option>,
+ * 2. render one explicit purchase card <button> per <option>,
* 3. on click -> set select.value + dispatch a native 'change' event so
* the existing jQuery `.on('change', '.single-option-selector', ...)'
* handler in custom.js.liquid (and everything downstream) fires exactly
@@ -20,6 +20,36 @@
var SELECTOR = 'select.single-option-selector';
+ function optionKind(text, sku) {
+ var value = ((text || '') + ' ' + (sku || '')).toLowerCase();
+ if (/sample/.test(value)) return 'sample';
+ if (/roll/.test(value)) return 'roll';
+ if (/bolt/.test(value)) return 'bolt';
+ if (/yard|metre|meter/.test(value)) return 'yardage';
+ return 'product';
+ }
+
+ function optionCopy(option) {
+ var raw = (option.textContent || option.value || '').trim();
+ var parts = raw.split(/\s+-\s+(?=\$)/);
+ var kind = optionKind(raw, option.getAttribute('data-sku'));
+ var title = kind === 'sample' ? 'Order a sample' :
+ kind === 'roll' ? 'Buy a roll' :
+ kind === 'bolt' ? 'Buy a bolt' :
+ kind === 'yardage' ? 'Buy by the yard' : 'Select this option';
+ return {
+ kind: kind,
+ title: title,
+ detail: parts[0] || raw,
+ price: parts.length > 1 ? parts.slice(1).join(' - ') : ''
+ };
+ }
+
+ window.DWPdpPurchaseOptions = {
+ optionKind: optionKind,
+ optionCopy: optionCopy
+ };
+
function buildPills(select) {
if (!select || select.dataset.dwPilled === '1') return;
// Only enhance real size/option selectors with 2+ choices.
@@ -34,18 +64,31 @@
if (styledShell) { styledShell.style.display = 'none'; }
var wrap = document.createElement('div');
- wrap.className = 'dw-size-pills';
+ wrap.className = 'dw-size-pills dw-purchase-options';
wrap.setAttribute('role', 'radiogroup');
var lbl = select.getAttribute('data-option-name') ||
(select.options[0] && select.options[0].closest ? '' : '');
- wrap.setAttribute('aria-label', lbl || 'Size');
+ wrap.setAttribute('aria-label', lbl && lbl.toLowerCase() !== 'title' ? lbl : 'Choose sample or full-size product');
+
+ var heading = document.createElement('p');
+ heading.className = 'dw-purchase-options__heading';
+ heading.textContent = 'Choose how you would like to order';
+ wrap.appendChild(heading);
opts.forEach(function (opt) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'dw-size-pill';
btn.setAttribute('role', 'radio');
- btn.textContent = (opt.textContent || opt.value || '').trim();
+ var copy = optionCopy(opt);
+ btn.classList.add('dw-size-pill--' + copy.kind);
+ btn.setAttribute('aria-label', [copy.title, copy.detail, copy.price].filter(Boolean).join(', '));
+ btn.innerHTML = '<span class="dw-size-pill__title"></span>' +
+ '<span class="dw-size-pill__detail"></span>' +
+ '<strong class="dw-size-pill__price"></strong>';
+ btn.querySelector('.dw-size-pill__title').textContent = copy.title;
+ btn.querySelector('.dw-size-pill__detail').textContent = copy.detail;
+ btn.querySelector('.dw-size-pill__price').textContent = copy.price;
btn.dataset.value = opt.value;
if (opt.disabled) {
btn.disabled = true;
@@ -108,10 +151,60 @@
Array.prototype.forEach.call(selects, buildPills);
}
+ function repairSearchLabels() {
+ var fields = document.querySelectorAll('input[aria-label*="Translation missing"], input[aria-label=""]');
+ Array.prototype.forEach.call(fields, function (field) {
+ if (field.type === 'search' || /search/i.test(field.name || field.placeholder || '')) {
+ field.setAttribute('aria-label', 'Search products');
+ }
+ });
+ }
+
+ function buildMobilePurchaseBar() {
+ if (!document.body.classList.contains('template-product') || document.querySelector('.dw-mobile-purchase')) return;
+ var nativeButton = document.querySelector('.product-add-to-cart .add-to-cart');
+ if (!nativeButton) return;
+ var bar = document.createElement('div');
+ bar.className = 'dw-mobile-purchase';
+ bar.setAttribute('aria-label', 'Product purchase actions');
+ bar.innerHTML = '<div class="dw-mobile-purchase__summary"><span>Selected option</span><strong></strong></div>' +
+ '<button type="button" class="dw-mobile-purchase__button">Add to cart</button>';
+ document.body.appendChild(bar);
+ var summary = bar.querySelector('strong');
+ var button = bar.querySelector('button');
+ var currentAction = nativeButton;
+
+ function sync() {
+ var active = document.querySelector('.dw-size-pill.is-active');
+ var title = active && active.querySelector('.dw-size-pill__title');
+ var price = active && active.querySelector('.dw-size-pill__price');
+ var isSample = active && active.classList.contains('dw-size-pill--sample');
+ var sampleButton = document.querySelector('.product-add-to-cart .dl-sample-btn:not(.unavailable), .product-add-to-cart .dl-second-sample-btn:not(.unavailable)');
+ currentAction = isSample && sampleButton ? sampleButton : nativeButton;
+ summary.textContent = [title && title.textContent, price && price.textContent].filter(Boolean).join(' · ') || 'Ready to order';
+ button.disabled = Boolean(currentAction.disabled || currentAction.classList.contains('disabled') || currentAction.classList.contains('unavailable'));
+ button.textContent = isSample ? 'Order sample' : (nativeButton.value || nativeButton.textContent || 'Add to cart');
+ }
+
+ button.addEventListener('click', function () {
+ currentAction.click();
+ });
+ document.addEventListener('change', function (event) {
+ if (event.target.matches && event.target.matches('.single-option-selector, .product-select')) setTimeout(sync, 0);
+ });
+ sync();
+ }
+
if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
+ document.addEventListener('DOMContentLoaded', function () {
+ init();
+ repairSearchLabels();
+ buildMobilePurchaseBar();
+ });
} else {
init();
+ repairSearchLabels();
+ buildMobilePurchaseBar();
}
// Re-run shortly after load in case theme JS injects/reorders selectors.
window.addEventListener('load', function () { setTimeout(init, 600); });
diff --git a/shopify/theme-LIVE-pull-20260728-colorbar/layout/theme.liquid b/shopify/theme-LIVE-pull-20260728-colorbar/layout/theme.liquid
index d9a024be..b7de4f9b 100644
--- a/shopify/theme-LIVE-pull-20260728-colorbar/layout/theme.liquid
+++ b/shopify/theme-LIVE-pull-20260728-colorbar/layout/theme.liquid
@@ -64,12 +64,6 @@
.contact { margin-top: 24px; font-size: 14px; color: #999; }
.contact a { color: #2563eb; text-decoration: none; }
</style>
-<script src="{{ 'dw-grid-control.js' | asset_url }}" defer></script>
-
-
-
-
-
<!-- cache-bust: 2026-03-25T00:54:55.724677 -->
</head>
<body>
← 3c6d03f6 block sample prices from roll imports
·
back to Dw Validator Debug TK11314
·
make free sample entitlement reliable 2ecca051 →