@clicktap/ui 0.40.8 → 0.40.10

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.
Files changed (40) hide show
  1. package/components/Accordion/Accordion.d.ts +1 -1
  2. package/components/Accordion/Accordion.js +1 -1
  3. package/components/Accordion/index.d.ts +2 -2
  4. package/components/Accordion/index.js +1 -1
  5. package/components/Badge/Badge.js +1 -1
  6. package/components/Badge/Badge.types.d.ts +1 -1
  7. package/components/Badge/index.js +1 -1
  8. package/components/Collection/CollectionFilters/CollectionFilters.js +1 -1
  9. package/components/Collection/CollectionFilters/index.js +1 -1
  10. package/components/Collection/index.js +1 -1
  11. package/components/Pagination/Pagination.d.ts +1 -1
  12. package/components/Pagination/Pagination.js +1 -1
  13. package/components/Pagination/index.js +1 -1
  14. package/components/Tooltip/Tooltip.d.ts +1 -1
  15. package/components/Tooltip/Tooltip.js +1 -1
  16. package/components/Tooltip/Tooltip.types.d.ts +1 -1
  17. package/components/Tooltip/index.js +1 -1
  18. package/docs/migrations/2026/01/2026-01-14-external-link-detection.md +290 -0
  19. package/docs/migrations/2026/01/2026-01-14-use-route-hook.md +142 -0
  20. package/docs/migrations/2026/01/2026-01-15-input-disable-skeleton.md +82 -0
  21. package/docs/migrations/2026/02/2026-02-16-env-utils-from-clicktap-ui.md +94 -0
  22. package/docs/migrations/2026/02/2026-02-17-select-popover-portal-container-restored.md +84 -0
  23. package/docs/migrations/2026/03/2026-03-24-image-dimmed-hidden-onready.md +108 -0
  24. package/docs/migrations/2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md +81 -0
  25. package/docs/migrations/2026/04/2026-04-17-select-address-input-onchange-key-or-item.md +182 -0
  26. package/docs/migrations/2026/04/2026-04-24-image-placeholder-frontend-fallback.md +85 -0
  27. package/docs/migrations/2026/04/2026-04-30-prune-invalid-pdp-selections.md +166 -0
  28. package/docs/migrations/2026/04/2026-04-30-top-down-pdp-option-dependency.md +286 -0
  29. package/docs/migrations/2026/05/2026-05-01-build-pdp-url-util.md +141 -0
  30. package/docs/migrations/2026/05/2026-05-03-auth-aware-graphql-client.md +278 -0
  31. package/docs/migrations/2026/05/2026-05-20-framer-motion-11-idioms.md +391 -0
  32. package/docs/migrations/2026/05/2026-05-21-button-loading-content.md +265 -0
  33. package/docs/migrations/2026/05/2026-05-21-password-check-hide-on-empty.md +50 -0
  34. package/docs/migrations/2026/05/2026-05-29-password-check-requirements-variant.md +63 -0
  35. package/docs/migrations/2026/06/2026-06-17-select-standardization.md +286 -0
  36. package/docs/migrations/2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md +75 -0
  37. package/docs/migrations/2026/07/2026-07-31-ui-exports-bundler-resolution.md +50 -0
  38. package/docs/migrations/2026/08/2026-08-02-nextui-to-heroui.md +65 -0
  39. package/docs/migrations/README.md +31 -0
  40. package/package.json +7 -5
@@ -0,0 +1,82 @@
1
+ # Input disableSkeleton Prop
2
+
3
+ **Date:** 2026-01-15
4
+ **Affects:** `@clicktap/ui`, `apps/frontend`
5
+ **Breaking:** No
6
+ **Classification:** IMPROVEMENT
7
+
8
+
9
+ ## Dependencies
10
+
11
+ *None*
12
+
13
+ ## Summary
14
+
15
+ Added a `disableSkeleton` prop to the Input component that allows consumers to skip the skeleton loading state during hydration. This is useful when you need immediate interactivity (e.g., auto-focus) on page load.
16
+
17
+ ## Background
18
+
19
+ The Input component uses `useIsClient()` to show a skeleton placeholder during server-side rendering and hydration. This prevents hydration mismatches but causes a brief "flash" where the input appears disabled/skeleton before becoming interactive.
20
+
21
+ For forms where immediate user interaction is expected (like login/authentication pages), this delay can feel sluggish.
22
+
23
+ ## Changes
24
+
25
+ ### New Prop
26
+
27
+ ```typescript
28
+ interface InputProps extends TextFieldProps {
29
+ // ... existing props
30
+
31
+ /**
32
+ * When true, renders the input immediately without showing a skeleton during hydration.
33
+ * Use this when the input doesn't need hydration protection (e.g., simple text inputs
34
+ * that don't have complex state or need immediate interactivity).
35
+ */
36
+ disableSkeleton?: boolean;
37
+ }
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### Before
43
+
44
+ ```tsx
45
+ <Input
46
+ name="email"
47
+ type="email"
48
+ label="Email"
49
+ autoFocus // Won't work - skeleton renders first
50
+ />
51
+ ```
52
+
53
+ ### After
54
+
55
+ ```tsx
56
+ <Input
57
+ name="email"
58
+ type="email"
59
+ label="Email"
60
+ disableSkeleton // Renders immediately
61
+ autoFocus // Now works as expected
62
+ />
63
+ ```
64
+
65
+ ## When to Use
66
+
67
+ **Use `disableSkeleton` when:**
68
+ - The input needs to auto-focus on page load
69
+ - Immediate interactivity is important for UX
70
+ - The input is a simple text/email/password field without complex state
71
+
72
+ **Don't use `disableSkeleton` when:**
73
+ - The input has complex state that could cause hydration mismatches
74
+ - The input relies on client-only data that isn't available during SSR
75
+ - You're unsure - the default skeleton behavior is safer
76
+
77
+ ## Migration Checklist
78
+
79
+ - [ ] Identify inputs that need immediate interactivity (e.g., login forms)
80
+ - [ ] Add `disableSkeleton` prop to those inputs
81
+ - [ ] Add `autoFocus` if the input should focus on page load
82
+ - [ ] Test that hydration works correctly (no console errors)
@@ -0,0 +1,94 @@
1
+ # Environment Utilities from @clicktap/ui
2
+
3
+ **Date:** 2026-02-16
4
+ **Affects:** All files importing `isDevelopment` / `isProduction` from local `utils/env`
5
+ **Classification:** IMPROVEMENT
6
+ **Breaking:** No
7
+
8
+ ## Dependencies
9
+
10
+ - [Payment Method Error Handling](../../../../../../apps/frontend/docs/migrations/2026/01/2026-01-14-payment-error-handling.md) — introduced the local `utils/env.ts`
11
+
12
+ ## Summary
13
+
14
+ The `isDevelopment` and `isProduction` boolean constants have been moved from the frontend's local `utils/env.ts` into `@clicktap/ui/utils/env`, making them available to all apps consuming `@clicktap/ui`. The local file has been deleted.
15
+
16
+ ## Problem
17
+
18
+ The `isDevelopment` / `isProduction` utility was defined locally in `apps/frontend/utils/env.ts`. Other apps (admin, future storefronts) that need the same pattern would have to duplicate it. Moving it into the shared UI library centralises the definition.
19
+
20
+ Additionally, the `@clicktap/ui` Vite build was replacing `process.env.NODE_ENV` with `"production"` at library build time, which meant any `NODE_ENV`-based guards inside the UI library (e.g., `console.warn` in the Image component) were dead-code-eliminated before consumers ever saw them. A `define` override in `libs/ui/vite.config.ts` now preserves `process.env.NODE_ENV` so the **consuming app's** bundler handles the replacement.
21
+
22
+ ## Changes
23
+
24
+ ### Vite library build fix
25
+
26
+ The `@clicktap/ui` Vite config now includes:
27
+
28
+ ```ts
29
+ define: {
30
+ 'process.env.NODE_ENV': 'process.env.NODE_ENV',
31
+ },
32
+ ```
33
+
34
+ This prevents Vite from baking `"production"` into the published bundle, so environment-guarded code (like dev-only `console.warn`) works correctly at the consumer's build time.
35
+
36
+ **Source:** `libs/ui/vite.config.ts`
37
+
38
+ ### New shared utility
39
+
40
+ ```ts
41
+ // @clicktap/ui/utils/env
42
+ export const isDevelopment = process.env.NODE_ENV === 'development';
43
+ export const isProduction = process.env.NODE_ENV === 'production';
44
+ ```
45
+
46
+ **Source:** `libs/ui/src/utils/env.ts`
47
+
48
+ ### Import change
49
+
50
+ **Before:**
51
+ ```ts
52
+ import { isDevelopment } from '../utils/env';
53
+ import { isDevelopment } from '../../../utils/env';
54
+ import { isDevelopment } from '../../../../../utils/env';
55
+ ```
56
+
57
+ **After:**
58
+ ```ts
59
+ import { isDevelopment } from '@clicktap/ui/utils/env';
60
+ ```
61
+
62
+ ### Files changed
63
+
64
+ | File | Change |
65
+ |------|--------|
66
+ | `apps/frontend/utils/env.ts` | Deleted |
67
+ | `providers/CybersourceMicroformV2Provider.tsx` | Import updated |
68
+ | `components/Checkout/ExpressCheckout/PaypalButton.tsx` | Import updated |
69
+ | `components/Checkout/ExpressCheckout/GooglePayButton.tsx` | Import updated |
70
+ | `components/Checkout/ExpressCheckout/ApplePayButton.tsx` | Import updated |
71
+ | `components/Checkout/ExpressCheckout/AmazonPayButton.tsx` | Import updated |
72
+ | `components/Checkout/Step/Review/CheckoutButton/ApplePayCheckoutButton.tsx` | Import updated |
73
+ | `components/Checkout/Step/Review/CheckoutButton/GooglePayCheckoutButton.tsx` | Import updated |
74
+ | `components/Checkout/Step/Review/CheckoutButton/PaypalCheckoutButton.tsx` | Import updated |
75
+ | `components/Checkout/Step/Review/CheckoutButton/AmazonCheckoutButton.tsx` | Import updated |
76
+ | `components/Checkout/Step/Payment/Authorizenet/AuthorizenetPaymentMethod.tsx` | Import updated |
77
+
78
+ ## Migration Steps
79
+
80
+ 1. Update `@clicktap/ui` to the version containing this change.
81
+ 2. Find all imports from the local `utils/env`:
82
+ ```bash
83
+ grep -r "from.*utils/env" apps/frontend/
84
+ ```
85
+ 3. Replace each import path with `@clicktap/ui/utils/env`.
86
+ 4. Delete `apps/frontend/utils/env.ts`.
87
+ 5. Run typecheck to confirm no broken imports.
88
+
89
+ ## Migration Checklist
90
+
91
+ - [ ] Update `@clicktap/ui` to latest
92
+ - [ ] Replace all `utils/env` imports with `@clicktap/ui/utils/env`
93
+ - [ ] Delete `apps/frontend/utils/env.ts`
94
+ - [ ] Run typecheck: `nx run frontend:typecheck`
@@ -0,0 +1,84 @@
1
+ # Select popoverPortalContainer Restored
2
+
3
+ **Date:** 2026-02-17
4
+ **Affects:** `@clicktap/ui` Select component, any consumer using Select inside Dialog or Modal
5
+ **Classification:** BUGFIX
6
+ **Breaking:** No
7
+
8
+ ## Dependencies
9
+
10
+ *None*
11
+
12
+ ## Summary
13
+
14
+ The `popoverPortalContainer` prop was incorrectly removed from the Select component on 2026-01-30 during the react-aria upgrade refactor. This prop controls where the Popover is portalled, which is essential when Select is used inside a Dialog — without it, the Popover renders into `document.body`, causing incorrect width calculation because both Dialog and Popover use react-aria's Modal internals. The prop has been restored using `UNSAFE_PortalProvider` from `react-aria`.
15
+
16
+ ## Problem
17
+
18
+ The react-aria upgrade (commits `7c27599` and `93ae6b3` on 2026-01-30) removed `popoverPortalContainer` in two steps:
19
+
20
+ 1. Commit `7c27599` correctly migrated from `UNSTABLE_portalContainer` (on Popover) to `UNSAFE_PortalProvider` (wrapper), preserving the feature
21
+ 2. Commit `93ae6b3` then removed the feature entirely — deleting the prop from types, removing the `UNSAFE_PortalProvider` import and conditional rendering
22
+
23
+ The migration guide in [Checkout Fixes — Section 6](../../../../../../apps/frontend/docs/migrations/2026/01/2026-01-14-checkout-fixes.md) was updated to document this removal as intentional. It was not — `popoverPortalContainer` was a working feature, not a deprecated one.
24
+
25
+ **If your codebase was updated following that migration guide**, you may have removed `popoverPortalContainer` usage from Select instances. Review git history around 2026-01-30 for removals:
26
+
27
+ ```bash
28
+ git log --all --oneline --after="2026-01-28" --before="2026-02-01" -- '*.tsx' '*.ts' | head -20
29
+ git log --all -p --after="2026-01-28" --before="2026-02-01" -S "popoverPortalContainer" -- '*.tsx'
30
+ ```
31
+
32
+ ## Changes
33
+
34
+ | File | What changed |
35
+ |------|-------------|
36
+ | `libs/ui/src/components/Select/Select.tsx` | Restored `UNSAFE_PortalProvider` import from `react-aria`; restored conditional portal wrapping |
37
+ | `libs/ui/src/components/Select/Select.types.ts` | Restored `popoverPortalContainer?: () => HTMLElement | null` prop |
38
+
39
+ ### Restored behavior
40
+
41
+ When `popoverPortalContainer` is provided, the Popover is wrapped in `UNSAFE_PortalProvider` which controls where the portal renders. When omitted, default react-aria behavior applies (portal to `document.body`).
42
+
43
+ ```tsx
44
+ // Select.tsx — restored conditional
45
+ {popoverPortalContainer ? (
46
+ <UNSAFE_PortalProvider getContainer={popoverPortalContainer}>
47
+ {popoverContent}
48
+ </UNSAFE_PortalProvider>
49
+ ) : (
50
+ popoverContent
51
+ )}
52
+ ```
53
+
54
+ **Source:** `libs/ui/src/components/Select/Select.tsx:209`
55
+
56
+ ## Migration Steps
57
+
58
+ 1. **If you removed `popoverPortalContainer` from Select usage** following the previous migration guide, restore it:
59
+
60
+ ```tsx
61
+ // Restore this where Select is used inside Dialog/Modal
62
+ <Select popoverPortalContainer={() => dialogRef.current}>
63
+ ```
64
+
65
+ 2. **Note the type change from the original API** — this was correctly updated during the react-aria upgrade and remains changed:
66
+
67
+ ```tsx
68
+ // Old (pre-upgrade)
69
+ popoverPortalContainer?: Element;
70
+
71
+ // Current (restored)
72
+ popoverPortalContainer?: () => HTMLElement | null;
73
+ ```
74
+
75
+ The prop now takes a **getter function** instead of a direct element reference.
76
+
77
+ 3. **The `UNSAFE_PortalProvider` import is from `react-aria`**, not `react-aria-components`. If you have custom components that also need portal control, import from the correct package.
78
+
79
+ ## Migration Checklist
80
+
81
+ - [ ] Search git history for removed `popoverPortalContainer` usage around 2026-01-30
82
+ - [ ] Restore `popoverPortalContainer` on any Select used inside Dialog, Modal, or other overlay contexts
83
+ - [ ] Update prop value from `Element` to `() => HTMLElement | null` getter if restoring pre-upgrade code
84
+ - [ ] Verify Select popover width matches trigger width when rendered inside Dialog
@@ -0,0 +1,108 @@
1
+ # Image: dimmed, hidden, and onReady Props
2
+
3
+ **Date:** 2026-03-24
4
+ **Affects:** `@clicktap/ui` — `Image` component
5
+ **Classification:** IMPROVEMENT
6
+ **Breaking:** No
7
+ **Group:** plp-image-transitions
8
+
9
+ ## Dependencies
10
+
11
+ *None*
12
+
13
+ See also: [Collection Image Filter Transitions](../../../../../../apps/frontend/docs/migrations/2026/03/2026-03-24-collection-image-filter-transitions.md) ↔ — frontend implementation using these props
14
+
15
+ ## Summary
16
+
17
+ The `Image` component gains three optional props for opacity-based loading transitions: `dimmed` (50% opacity), `hidden` (0% opacity), and `onReady` (callback when a new image loads after src change). When none of these props are passed, the component behaves exactly as before (blur-based loading). This also fixes a flicker bug caused by `onError` firing on valid images.
18
+
19
+ ## Problem
20
+
21
+ Two issues existed:
22
+
23
+ 1. **No way to coordinate image transitions** — collection grids needed to dim/hide images during filter changes to prevent stale variant images from flashing. The `Image` component had no props for external opacity control.
24
+
25
+ 2. **onError flicker** — Next.js image optimization can fire `onError` on valid images when one `srcSet` variant fails while another succeeds. The immediate `setErrorSrc` call caused a re-render that produced a visible flicker. Additionally, `key={srcKey}` on the error boundary caused a full component remount on every src change.
26
+
27
+ ## Changes
28
+
29
+ ### New props on `ExtendedImageProps`
30
+
31
+ ```typescript
32
+ export type ExtendedImageProps = ImageProps & {
33
+ /** Reduce opacity to 50% (e.g., while waiting for data) */
34
+ dimmed?: boolean;
35
+ /** Hide image completely — opacity 0 (e.g., before grid rearrange) */
36
+ hidden?: boolean;
37
+ /** Fires when a new image has loaded after a src change */
38
+ onReady?: () => void;
39
+ };
40
+ ```
41
+
42
+ **Source**: `libs/ui/src/components/Image/Image.tsx:62-69`
43
+
44
+ ### Managed vs unmanaged mode
45
+
46
+ When `dimmed`, `hidden`, or `onReady` are passed ("managed" mode):
47
+ - Opacity transitions replace the blur loading effect
48
+ - `opacity: 0` when hidden, `0.5` when loading or dimmed, `1` when loaded
49
+ - Src changes are detected during render (not `useEffect`) to prevent stale frame flash
50
+ - `onReady` fires when a new image finishes loading
51
+
52
+ When none are passed ("unmanaged" mode):
53
+ - Existing blur-based loading (`blur-md`) is preserved
54
+ - No opacity management, no src change detection
55
+ - Fully backward compatible
56
+
57
+ ### onError debounce (bugfix)
58
+
59
+ `onError` now uses a 2-second debounced timeout. If `onLoad` fires within that window (which it does for valid images), the timeout is cleared and no error fallback occurs.
60
+
61
+ **Before:**
62
+ ```tsx
63
+ onError={() => setErrorSrc(src)}
64
+ ```
65
+
66
+ **After:**
67
+ ```tsx
68
+ onError={() => {
69
+ errorTimer.current = setTimeout(() => setErrorSrc(src), 2000);
70
+ }}
71
+ onLoad={(e) => {
72
+ clearTimeout(errorTimer.current);
73
+ setLoadingImg(false);
74
+ // ...
75
+ }}
76
+ ```
77
+
78
+ ### Error boundary key removal (bugfix)
79
+
80
+ The `key={srcKey}` on `ImageErrorBoundary` has been removed. It was causing a full component remount on every src change, which destroyed state and produced flicker. The error boundary's `errorSrc` state check already handles src changes correctly.
81
+
82
+ ## Migration Steps
83
+
84
+ No action required — these are additive, non-breaking changes. Existing `Image` usage is unaffected.
85
+
86
+ To use the new props:
87
+
88
+ ```tsx
89
+ import { Image } from '@clicktap/ui/components/Image';
90
+
91
+ <Image
92
+ alt="Product"
93
+ src={imageUrl}
94
+ width={500}
95
+ height={625}
96
+ dimmed={isLoading} // optional: 50% opacity
97
+ hidden={isHidden} // optional: 0% opacity
98
+ onReady={() => onLoaded()} // optional: callback when loaded
99
+ />
100
+ ```
101
+
102
+ ## Migration Checklist
103
+
104
+ - [ ] Verify existing `Image` usage still works (blur loading, error fallback)
105
+ - [ ] Verify `dimmed` prop reduces opacity to 50%
106
+ - [ ] Verify `hidden` prop hides image completely (opacity 0)
107
+ - [ ] Verify `onReady` fires after src change + image load
108
+ - [ ] Verify no flicker on images that previously flickered with `onError`
@@ -0,0 +1,81 @@
1
+ # ModalOverlay: Fix Dismiss on Backdrop Click
2
+
3
+ **Date:** 2026-03-24
4
+ **Affects:** `@clicktap/ui` — `ModalOverlay` component
5
+ **Classification:** BUGFIX
6
+ **Breaking:** No
7
+
8
+ ## Dependencies
9
+
10
+ *None*
11
+
12
+ ## Summary
13
+
14
+ Clicking the backdrop overlay behind a drawer or modal did not dismiss it, even with `isDismissable` set. Pressing Escape and the close button worked correctly. The fix adds an explicit `onPointerDown` handler to the `ModalOverlay` that closes when the user clicks directly on the backdrop.
15
+
16
+ ## Problem
17
+
18
+ react-aria's `useInteractOutside` hook detects clicks **outside** a ref element to trigger dismiss. However, the `ModalOverlay` component covers the entire viewport — so every click, including on the backdrop, is considered "inside" the overlay ref. The dismiss never fires for backdrop clicks.
19
+
20
+ The `Modal` component already had `pointer-events-none [&>*]:pointer-events-auto` to let clicks pass through to the overlay, but react-aria's event detection still could not distinguish backdrop clicks from content clicks.
21
+
22
+ ## Changes
23
+
24
+ **File**: `libs/ui/src/components/ModalOverlay/ModalOverlay.tsx`
25
+
26
+ The `InnerModalOverlay` component now:
27
+
28
+ 1. Extracts `onOpenChange` from props (previously spread via `...restProps`)
29
+ 2. Adds a `handlePointerDown` callback that calls `onOpenChange(false)` when the click target is the overlay element itself (`e.target === e.currentTarget`)
30
+ 3. Passes both `onOpenChange` and `onPointerDown` to the overlay element
31
+
32
+ ```tsx
33
+ const {
34
+ key,
35
+ shouldCloseOnInteractOutside: propShouldClose,
36
+ onOpenChange,
37
+ ...restProps
38
+ } = props;
39
+
40
+ // react-aria's useInteractOutside checks clicks outside the overlay ref,
41
+ // but since the overlay covers the viewport, backdrop clicks are "inside"
42
+ // and never trigger dismiss. Handle backdrop clicks explicitly.
43
+ const handlePointerDown = useCallback(
44
+ (e: PointerEvent<HTMLDivElement>) => {
45
+ if (isDismissable && e.target === e.currentTarget && onOpenChange) {
46
+ onOpenChange(false);
47
+ }
48
+ },
49
+ [isDismissable, onOpenChange],
50
+ );
51
+
52
+ const commonProps = {
53
+ isDismissable,
54
+ shouldCloseOnInteractOutside,
55
+ onOpenChange,
56
+ onPointerDown: handlePointerDown,
57
+ // ...rest
58
+ };
59
+ ```
60
+
61
+ **Source**: `libs/ui/src/components/ModalOverlay/ModalOverlay.tsx:139-183`
62
+
63
+ The `e.target === e.currentTarget` check ensures that only clicks directly on the backdrop trigger dismiss — clicks on child elements (the drawer/dialog content) are ignored because `e.target` would be a descendant, not the overlay itself.
64
+
65
+ ## Migration Steps
66
+
67
+ 1. Update `libs/ui/src/components/ModalOverlay/ModalOverlay.tsx`:
68
+ - Add `PointerEvent` to the React type imports
69
+ - Destructure `onOpenChange` from props alongside `key` and `shouldCloseOnInteractOutside`
70
+ - Add the `handlePointerDown` callback
71
+ - Add `onOpenChange` and `onPointerDown: handlePointerDown` to `commonProps`
72
+
73
+ 2. Verify by opening any drawer (e.g., cart drawer) and clicking the dark backdrop area — it should now close.
74
+
75
+ ## Migration Checklist
76
+
77
+ - [ ] Update `ModalOverlay.tsx` with the `handlePointerDown` handler
78
+ - [ ] Verify drawers dismiss on backdrop click
79
+ - [ ] Verify clicking inside a drawer does **not** dismiss it
80
+ - [ ] Verify Escape key still dismisses
81
+ - [ ] Verify close button still works
@@ -0,0 +1,182 @@
1
+ # Select / AddressInput: `onSelectionChange` / `onChange` callback widened to `Key | T`
2
+
3
+ **Date:** 2026-04-17 *(documentation date; underlying react-aria upgrade landed 2026-01-19 via commit `7c27599b2a`)*
4
+ **Affects:** Frontend apps consuming `@clicktap/ui`'s `Select` and components that re-export its types (`AddressInput`, etc.). Any handler typed as `(value: Key) => void` breaks after the update.
5
+ **Classification:** COMPATIBILITY
6
+ **Breaking:** Yes — TypeScript-only. No runtime change; existing handlers still receive the same value at runtime, but strict TS setups fail to compile.
7
+
8
+ ## Dependencies
9
+
10
+ | Package | Minimum version |
11
+ |---|---|
12
+ | `@clicktap/ui` | The release that bundles `react-aria-components ^1.12.x` (commit `7c27599b2a`). Confirm with `pnpm why react-aria-components` that the resolved version is `>= 1.12.0`. |
13
+
14
+ ## Summary
15
+
16
+ `@clicktap/ui`'s `Select<T>` extends `AriaComboBoxProps<T>`. After the react-aria upgrade, the `onSelectionChange` argument type widened from `Key | null` to effectively `Key | null | T` — callers can now receive either the selected item's key **or**, in certain configurations, the full item object, depending on how the consumer sets up `items` and `selectedKey`.
17
+
18
+ For consumer handlers that were strictly typed `(key: Key) => void`, TypeScript now complains that `Key | T` is not assignable to `Key`. The fix is to narrow at the callback entry point.
19
+
20
+ ## Consumer Impact
21
+
22
+ Types changed:
23
+
24
+ | Type | Before | After |
25
+ |---|---|---|
26
+ | `SelectProps<T>['onSelectionChange']` | `(key: Key \| null) => void` | `(key: Key \| null \| T) => void` (widened union) |
27
+ | `AddressInputProps['onChange']` (in `apps/frontend/components/AddressForm/AddressInput`) | Re-typed to `(address?: AddressSuggestion) => void` — consumer-owned. Still works after upgrade if the inner `onSelectionChange` handler narrows correctly. |
28
+
29
+ Find affected handlers:
30
+
31
+ ```bash
32
+ # Handlers passed to Select or wrappers that assume `Key` only
33
+ grep -rn 'onSelectionChange\|onChange=' apps/frontend/ | \
34
+ grep -E 'Select|AddressInput|Combobox|Dropdown' | \
35
+ head -40
36
+ ```
37
+
38
+ ## Preconditions
39
+
40
+ - [ ] `@clicktap/ui` is pinned to the release containing commit `7c27599b2a` or later. Check with:
41
+ ```bash
42
+ jq '.dependencies["@clicktap/ui"]' apps/frontend/package.json
43
+ ```
44
+ - [ ] `apps/frontend/package.json` is tracked in git. Some legacy project layouts gitignore it — de-ignore before applying changes so CI/typecheck runs see the fix.
45
+
46
+ ## Migration Steps
47
+
48
+ ### 1. Narrow the argument in your handler
49
+
50
+ The cleanest fix is a type guard at the callback boundary:
51
+
52
+ ```tsx
53
+ // Before — TS error after upgrade
54
+ <Select
55
+ items={items}
56
+ onSelectionChange={(key: Key) => {
57
+ selectItemByKey(key);
58
+ }}
59
+ />
60
+
61
+ // After — narrow the widened union
62
+ <Select
63
+ items={items}
64
+ onSelectionChange={(keyOrItem) => {
65
+ const key = typeof keyOrItem === 'string' || typeof keyOrItem === 'number'
66
+ ? keyOrItem
67
+ : keyOrItem?.id; // or whatever your T's key field is
68
+ if (key === null || key === undefined) return;
69
+ selectItemByKey(key);
70
+ }}
71
+ />
72
+ ```
73
+
74
+ > **For consumers with 10+ call sites:** the per-site narrow above gets repetitive. Consider extracting a local helper to your project's `utils/` module:
75
+ >
76
+ > ```ts
77
+ > import type { Key } from 'react-aria-components';
78
+ >
79
+ > export function narrowKey<T extends { id: Key }>(
80
+ > value: Key | T | null,
81
+ > ): Key | null {
82
+ > if (value === null) return null;
83
+ > if (typeof value === 'string' || typeof value === 'number') return value;
84
+ > return value.id;
85
+ > }
86
+ > ```
87
+ >
88
+ > Then each call site becomes `onSelectionChange={(v) => { const key = narrowKey(v); if (key !== null) selectItemByKey(key); }}`. We may upstream this helper to `@clicktap/ui/utils` in a future release; for now, ship it locally if your codebase has enough call sites to justify the abstraction.
89
+
90
+ ### 2. If you need the full item, accept it directly
91
+
92
+ When a handler wants the full `T`, skip the narrow and type against it:
93
+
94
+ ```tsx
95
+ type Option = { id: Key; label: string };
96
+
97
+ <Select<Option>
98
+ items={options}
99
+ onSelectionChange={(value) => {
100
+ // value is Key | null | Option — narrow by shape
101
+ const option = typeof value === 'object' && value !== null ? value : null;
102
+ if (option) onOptionSelected(option);
103
+ }}
104
+ />
105
+ ```
106
+
107
+ ### 3. For wrappers like `AddressInput`, narrow inside the wrapper once
108
+
109
+ If your app wraps `Select` (the clicktap `AddressInput` does this), narrow in one place so every consumer of the wrapper keeps its old `(address?: T) => void` contract:
110
+
111
+ ```tsx
112
+ // apps/frontend/components/AddressForm/AddressInput/AddressInput.tsx
113
+ <Select<AddressSuggestion>
114
+ items={suggestions}
115
+ onSelectionChange={(keyOrItem) => {
116
+ const selection = typeof keyOrItem === 'object' && keyOrItem !== null
117
+ ? keyOrItem
118
+ : suggestions.find((s) => s.key === keyOrItem);
119
+ props.onChange?.(selection);
120
+ }}
121
+ />
122
+ ```
123
+
124
+ ### Verification
125
+
126
+ ```bash
127
+ # Typecheck should pass with zero errors mentioning onSelectionChange
128
+ nx run frontend:typecheck
129
+ ```
130
+
131
+ ## Consumer-Project Workaround
132
+
133
+ If for any reason the consumer project can't yet take the new `@clicktap/ui` release but must compile:
134
+
135
+ 1. **Pin the previous minor** in `apps/frontend/package.json`:
136
+ ```json
137
+ "@clicktap/ui": "~X.Y.Z"
138
+ ```
139
+ Replace `X.Y.Z` with the last minor before the react-aria 3.43.x bump.
140
+
141
+ 2. **Or wrap locally** until ready — shadow `Select` in a local component that narrows the callback before forwarding:
142
+ ```tsx
143
+ // apps/frontend/components/LegacySelect.tsx
144
+ import { Select as UpstreamSelect, type SelectProps } from '@clicktap/ui/components/Select';
145
+ import type { Key } from 'react-aria-components';
146
+
147
+ type LegacySelectProps<T extends { id: Key }> = Omit<SelectProps<T>, 'onSelectionChange'> & {
148
+ onSelectionChange?: (key: Key | null) => void;
149
+ };
150
+
151
+ export function LegacySelect<T extends { id: Key }>(props: LegacySelectProps<T>) {
152
+ return (
153
+ <UpstreamSelect<T>
154
+ {...props}
155
+ onSelectionChange={(v) => {
156
+ const key = typeof v === 'object' && v !== null ? v.id : v;
157
+ props.onSelectionChange?.(key ?? null);
158
+ }}
159
+ />
160
+ );
161
+ }
162
+ ```
163
+
164
+ Prefer the narrow-in-handler fix over the wrapper — the wrapper is only for bulk migrations where touching every call site at once isn't feasible.
165
+
166
+ ## Rollback
167
+
168
+ Revert `@clicktap/ui` in `apps/frontend/package.json`:
169
+
170
+ ```bash
171
+ docker compose exec -u app node-app pnpm --filter frontend add '@clicktap/ui@<previous-minor>'
172
+ ```
173
+
174
+ Safe — TypeScript-only change. No persistent state involved.
175
+
176
+ ## References
177
+
178
+ - Upstream commit: `7c27599b2a` — `refactor(ui): upgrade react-aria and refactor Select component`
179
+ - react-aria upgrade: `react-aria ^3.43.1`, `react-aria-components ^1.12.2`
180
+ - Existing related guide: `docs/migrations/2026-01-14-checkout-fixes.md` (partially documents this upgrade; expand with the onSelectionChange narrowing pattern above)
181
+ - Source: `libs/ui/src/components/Select/Select.types.ts`
182
+ - Wrapper reference: `apps/frontend/components/AddressForm/AddressInput/AddressInput.types.ts`