@clicktap/ui 2.0.3 → 2.0.4
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/components/Collection/CollectionToolbar/CollectionToolbar.js +1 -1
- package/components/Collection/ProductCardSwatches/ProductCardSwatches.js +1 -1
- package/docs/migrations/2026/08/ui-2026-08-22-collection-accessibility-fixes.md +28 -0
- package/docs/migrations/index.json +10 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsxs as i,jsx as l}from"react/jsx-runtime";import{Option as p}from"../../Select/Option.js";import{Select as x}from"../../Select/Select.js";import{Button as h}from"../../Button/Button.js";import"../../../utils/cn.js";import{FilterIcon as
|
|
1
|
+
"use client";import{jsxs as i,jsx as l}from"react/jsx-runtime";import{Option as p}from"../../Select/Option.js";import{Select as x}from"../../Select/Select.js";import{Button as h}from"../../Button/Button.js";import"../../../utils/cn.js";import{FilterIcon as u}from"../../Icon/FilterIcon.js";import"react-stately";import"react";import"../../Select/ButtonSelect.js";import"react-aria";import"@react-aria/interactions";import"../../Select/parts.js";import"react-aria-components";import"../../Select/ComboBoxSelect.js";import"../../Loader/Pulse.js";import"../../Select/MultiButtonSelect.js";function D({totalItems:o,startItem:s,endItem:m,setShowMobileFilters:n,availableSortOrders:r,appliedSortOrder:e,onSortChange:a}){const c=[...e?[e]:[],...r];return i("div",{className:"flex flex-col gap-y-5 mb-3",children:[i("div",{className:"flex items-center justify-between lg:justify-end gap-x-2",children:[i(h,{className:"lg:hidden grow-0 shrink-0 basis-auto gap-x-3 px-4",size:"sm",variant:"outline",onPress:()=>n(!0),children:[l(u,{className:"w-3 h-3"}),"Filters"]}),l(x,{className:"w-44",selectedKey:e?`${e.code}_${e.direction}`:null,onSelectionChange:a,placeholder:"Sort by",label:"Sort By:",classNames:{input:"text-xs w-44 h-8 min-h-0",value:"text-xs",label:"hidden lg:block text-xs text-gray-600 whitespace-nowrap"},children:c.map(t=>l(p,{textValue:t.label,children:t.label},`${t.code}_${t.direction}`))})]}),o>0&&i("p",{className:"text-sm text-fg-muted",children:["Displaying ",s," - ",m," of ",o," items"]})]})}export{D as CollectionToolbar};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as c,jsxs as I,Fragment as R}from"react/jsx-runtime";import{Button as p}from"react-aria-components";import{cn as b}from"../../../utils/cn.js";import{CheckIcon as M}from"../../Icon/CheckIcon.js";import{isDevelopment as x}from"../../../utils/env.js";import{isValidHex as P,isVeryLight as j}from"../../../utils/color.js";import{isSwatchRenderer as y,SwatchRenderer as $,isValidImageUrl as _}from"../../../utils/swatch.js";function L(d){const s=new Map;for(const a of d)if("renderer"in a&&y(a.renderer)&&"options"in a)for(const r of a.options)r.swatch&&s.set(r.id,{...r.swatch,attributeCode:a.code,optionCode:r.code});return s}function T(d){const s=new Set;for(const a of d)if(a.simpleProduct?.saleability?.isSaleable!==!1)for(const r of a.attributes)s.add(`${r.attribute.code}:${r.option.code}`);return s}function V(d,s,a,r){const m=new Set,h=[];for(const l of d)for(const i of l.attributes){const f=s.get(i.option.id);f&&!m.has(i.option.id)&&a.has(i.attribute.code)&&(m.add(i.option.id),h.push({optionId:i.option.id,optionCode:f.optionCode,attributeCode:i.attribute.code,label:i.option.label,swatch:f,image:l.simpleProduct.image??null,isSaleable:r.has(`${i.attribute.code}:${i.option.code}`)}))}return h.sort((l,i)=>l.isSaleable===i.isSaleable?0:l.isSaleable?-1:1),h}function O({variants:d,availableFilters:s,attributeSwatchConfigs:a=[],matchedOptionIds:r,selectedOptionId:m,onSelect:h}){const l=new Map;for(const e of a)e.swatch?.listRenderer&&(x&&!y(e.swatch.listRenderer)&&console.warn(`[Swatch] Unknown listRenderer "${e.swatch.listRenderer}" for attribute "${e.code}" — skipping list swatches`),l.set(e.code,e.swatch.listRenderer));const i=L(s),f=T(d),u=V(d,i,l,f);if(u.length===0)return null;const U=new Set(u.map(e=>e.optionId)),k=r.find(e=>U.has(e)),N=m??k??u[0]?.optionId??null,S=u[0]?l.get(u[0].attributeCode)??null:null;return c("div",{className:"flex flex-row flex-wrap items-center gap-
|
|
1
|
+
"use client";import{jsx as c,jsxs as I,Fragment as R}from"react/jsx-runtime";import{Button as p}from"react-aria-components";import{cn as b}from"../../../utils/cn.js";import{CheckIcon as M}from"../../Icon/CheckIcon.js";import{isDevelopment as x}from"../../../utils/env.js";import{isValidHex as P,isVeryLight as j}from"../../../utils/color.js";import{isSwatchRenderer as y,SwatchRenderer as $,isValidImageUrl as _}from"../../../utils/swatch.js";function L(d){const s=new Map;for(const a of d)if("renderer"in a&&y(a.renderer)&&"options"in a)for(const r of a.options)r.swatch&&s.set(r.id,{...r.swatch,attributeCode:a.code,optionCode:r.code});return s}function T(d){const s=new Set;for(const a of d)if(a.simpleProduct?.saleability?.isSaleable!==!1)for(const r of a.attributes)s.add(`${r.attribute.code}:${r.option.code}`);return s}function V(d,s,a,r){const m=new Set,h=[];for(const l of d)for(const i of l.attributes){const f=s.get(i.option.id);f&&!m.has(i.option.id)&&a.has(i.attribute.code)&&(m.add(i.option.id),h.push({optionId:i.option.id,optionCode:f.optionCode,attributeCode:i.attribute.code,label:i.option.label,swatch:f,image:l.simpleProduct.image??null,isSaleable:r.has(`${i.attribute.code}:${i.option.code}`)}))}return h.sort((l,i)=>l.isSaleable===i.isSaleable?0:l.isSaleable?-1:1),h}function O({variants:d,availableFilters:s,attributeSwatchConfigs:a=[],matchedOptionIds:r,selectedOptionId:m,onSelect:h}){const l=new Map;for(const e of a)e.swatch?.listRenderer&&(x&&!y(e.swatch.listRenderer)&&console.warn(`[Swatch] Unknown listRenderer "${e.swatch.listRenderer}" for attribute "${e.code}" — skipping list swatches`),l.set(e.code,e.swatch.listRenderer));const i=L(s),f=T(d),u=V(d,i,l,f);if(u.length===0)return null;const U=new Set(u.map(e=>e.optionId)),k=r.find(e=>U.has(e)),N=m??k??u[0]?.optionId??null,S=u[0]?l.get(u[0].attributeCode)??null:null;return c("div",{className:"flex flex-row flex-wrap items-center gap-2 mt-1.5",children:u.map(e=>{const o=N===e.optionId,w=e.isSaleable?e.label:`${e.label} (out of stock)`;if(S===$.IMAGE)return c(p,{"aria-label":w,className:"cursor-pointer p-0",onPress:()=>{h(e.optionId,e.optionCode,e.image,e.attributeCode)},children:({isHovered:t})=>c("span",{className:b("block w-8 aspect-[2.2/1] border bg-contain bg-center bg-no-repeat transition-all duration-150",o?"border-fg ring-1 ring-fg":"border-border-subtle",t&&!o&&"border-border-emphasis"),title:w,style:e.swatch.imageUrl?{backgroundImage:`url(${e.swatch.imageUrl})`}:void 0})},e.optionId);if(S===$.TEXT)return c(p,{"aria-label":w,className:"cursor-pointer p-0",onPress:()=>{h(e.optionId,e.optionCode,e.image,e.attributeCode)},children:({isHovered:t})=>c("span",{className:b("inline-flex items-center px-1.5 py-0.5 text-[10px] font-medium border transition-colors duration-150",o?"bg-surface-inverse text-white border-fg":"bg-white text-slate-700 border-border",t&&!o&&"border-slate-500"),children:e.swatch.text??e.label})},e.optionId);const n=_(e.swatch.imageUrl),C=P(e.swatch.hex);if(!n&&!C){if(x){const t=[];e.swatch.hex&&!C&&t.push(`invalid hex "${e.swatch.hex}"`),e.swatch.imageUrl&&!n&&t.push(`invalid imageUrl "${e.swatch.imageUrl}"`),!e.swatch.hex&&!e.swatch.imageUrl&&t.push("missing hex and imageUrl"),console.warn(`[Swatch] Skipping list swatch for attribute "${e.attributeCode}" option "${e.optionCode}" — ${t.join("; ")}`)}return null}x&&e.swatch.imageUrl&&!n&&console.warn(`[Swatch] Falling back to SWATCH (hex) renderer for attribute "${e.attributeCode}" option "${e.optionCode}" — invalid imageUrl "${e.swatch.imageUrl}"`);const g=n?!1:j(e.swatch.hex??null),v=n?{backgroundImage:`url(${e.swatch.imageUrl})`}:{backgroundColor:e.swatch.hex??void 0};return c(p,{"aria-label":w,className:"cursor-pointer size-4 flex items-center justify-center p-0",onPress:()=>{h(e.optionId,e.optionCode,e.image,e.attributeCode)},children:({isHovered:t})=>I("span",{className:b("relative inline-flex items-center justify-center size-4 rounded-full transition-all duration-150 overflow-hidden",n&&"bg-cover bg-center",n&&o&&"ring-2 ring-inset ring-white shadow-[0_0_0_2px_#222]",n&&t&&!o&&"ring-1 ring-border-emphasis",!n&&g&&"border border-border"),style:v,children:[!e.isSaleable&&I(R,{children:[c("span",{className:b("absolute top-1/2 left-1/2 w-[141%] h-[1.5px] -translate-x-1/2 -translate-y-1/2 -rotate-45",g?"bg-slate-400":"bg-white drop-shadow-sm",t&&!o&&"opacity-70")}),(o||t)&&c("span",{className:b("absolute top-1/2 left-1/2 w-[141%] h-[1.5px] -translate-x-1/2 -translate-y-1/2 rotate-45",g?"bg-slate-400":"bg-white drop-shadow-sm",t&&!o&&"opacity-70")})]}),!n&&e.isSaleable&&(o||t)&&c(M,{className:b("w-2.5 h-2.5 transition-opacity duration-150",g?"text-fg-subtle":"text-white drop-shadow-sm",t&&!o&&"opacity-70")})]})},e.optionId)})})}export{O as ProductCardSwatches};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# CollectionToolbar and ProductCardSwatches Accessibility Fixes
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-22
|
|
4
|
+
**Package:** `@clicktap/ui`
|
|
5
|
+
**Affects:** `Collection/CollectionToolbar`, `Collection/ProductCardSwatches`
|
|
6
|
+
**Classification:** BUGFIX (accessibility)
|
|
7
|
+
**Breaking:** No. Visual change is 2px of extra gap between card swatches.
|
|
8
|
+
**Ticket:** CTAP-1380
|
|
9
|
+
|
|
10
|
+
## Changes
|
|
11
|
+
|
|
12
|
+
**`CollectionToolbar` — invalid `aria-details` removed.** The mobile filter button carried
|
|
13
|
+
`aria-details="show filters"`. `aria-details` takes an **ID reference**, not text, so axe
|
|
14
|
+
flags the value as invalid (`aria-valid-attr-value`). The button already has a visible
|
|
15
|
+
"Filters" label, so the attribute was redundant too.
|
|
16
|
+
|
|
17
|
+
**`ProductCardSwatches` — swatch spacing meets WCAG 2.5.8.** The swatches are 16px, under
|
|
18
|
+
the 24px minimum target size. The standard's spacing exception allows a smaller target
|
|
19
|
+
when no neighbouring target falls inside a 24px-diameter circle centred on it — centres
|
|
20
|
+
at least 24px apart. The row used `gap-1.5` (6px), leaving centres 22px apart, so every
|
|
21
|
+
swatch failed `target-size` (37 elements on a single collection page). Now `gap-2` (8px),
|
|
22
|
+
which puts centres at exactly 24px. The swatches themselves are unchanged.
|
|
23
|
+
|
|
24
|
+
## What to check
|
|
25
|
+
|
|
26
|
+
Swatch rows are 2px wider per gap — confirm no wrapping regression at the narrowest
|
|
27
|
+
breakpoint. Consumers overriding the swatch container's spacing should re-check they
|
|
28
|
+
still satisfy the 24px centre-to-centre rule.
|
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
"affects": "every consumer of react-aria-components — dependency policy, not an API change",
|
|
11
11
|
"read_before_updating": "never bump `react-aria`, `react-aria-components`, or `react-stately` independently — skewed internals fail silently, and only in production bundles."
|
|
12
12
|
},
|
|
13
|
+
{
|
|
14
|
+
"path": "2026/08/ui-2026-08-22-collection-accessibility-fixes.md",
|
|
15
|
+
"module": "libs/frontend-modules/ui",
|
|
16
|
+
"package": "@clicktap/ui",
|
|
17
|
+
"date": "2026-08-22",
|
|
18
|
+
"title": "CollectionToolbar and ProductCardSwatches Accessibility Fixes",
|
|
19
|
+
"classification": "BUGFIX (accessibility)",
|
|
20
|
+
"breaking": "No. Visual change is 2px of extra gap between card swatches.",
|
|
21
|
+
"affects": "`Collection/CollectionToolbar`, `Collection/ProductCardSwatches`"
|
|
22
|
+
},
|
|
13
23
|
{
|
|
14
24
|
"path": "2026/08/ui-2026-08-22-accordion-divider-class-ownership.md",
|
|
15
25
|
"module": "libs/frontend-modules/ui",
|