@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.
- package/components/Accordion/Accordion.d.ts +1 -1
- package/components/Accordion/Accordion.js +1 -1
- package/components/Accordion/index.d.ts +2 -2
- package/components/Accordion/index.js +1 -1
- package/components/Badge/Badge.js +1 -1
- package/components/Badge/Badge.types.d.ts +1 -1
- package/components/Badge/index.js +1 -1
- package/components/Collection/CollectionFilters/CollectionFilters.js +1 -1
- package/components/Collection/CollectionFilters/index.js +1 -1
- package/components/Collection/index.js +1 -1
- package/components/Pagination/Pagination.d.ts +1 -1
- package/components/Pagination/Pagination.js +1 -1
- package/components/Pagination/index.js +1 -1
- package/components/Tooltip/Tooltip.d.ts +1 -1
- package/components/Tooltip/Tooltip.js +1 -1
- package/components/Tooltip/Tooltip.types.d.ts +1 -1
- package/components/Tooltip/index.js +1 -1
- package/docs/migrations/2026/01/2026-01-14-external-link-detection.md +290 -0
- package/docs/migrations/2026/01/2026-01-14-use-route-hook.md +142 -0
- package/docs/migrations/2026/01/2026-01-15-input-disable-skeleton.md +82 -0
- package/docs/migrations/2026/02/2026-02-16-env-utils-from-clicktap-ui.md +94 -0
- package/docs/migrations/2026/02/2026-02-17-select-popover-portal-container-restored.md +84 -0
- package/docs/migrations/2026/03/2026-03-24-image-dimmed-hidden-onready.md +108 -0
- package/docs/migrations/2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md +81 -0
- package/docs/migrations/2026/04/2026-04-17-select-address-input-onchange-key-or-item.md +182 -0
- package/docs/migrations/2026/04/2026-04-24-image-placeholder-frontend-fallback.md +85 -0
- package/docs/migrations/2026/04/2026-04-30-prune-invalid-pdp-selections.md +166 -0
- package/docs/migrations/2026/04/2026-04-30-top-down-pdp-option-dependency.md +286 -0
- package/docs/migrations/2026/05/2026-05-01-build-pdp-url-util.md +141 -0
- package/docs/migrations/2026/05/2026-05-03-auth-aware-graphql-client.md +278 -0
- package/docs/migrations/2026/05/2026-05-20-framer-motion-11-idioms.md +391 -0
- package/docs/migrations/2026/05/2026-05-21-button-loading-content.md +265 -0
- package/docs/migrations/2026/05/2026-05-21-password-check-hide-on-empty.md +50 -0
- package/docs/migrations/2026/05/2026-05-29-password-check-requirements-variant.md +63 -0
- package/docs/migrations/2026/06/2026-06-17-select-standardization.md +286 -0
- package/docs/migrations/2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md +75 -0
- package/docs/migrations/2026/07/2026-07-31-ui-exports-bundler-resolution.md +50 -0
- package/docs/migrations/2026/08/2026-08-02-nextui-to-heroui.md +65 -0
- package/docs/migrations/README.md +31 -0
- 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`
|