@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,286 @@
|
|
|
1
|
+
# Select standardization — one `<Select searchable>`, `DropdownSelect` removed
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-17
|
|
4
|
+
**Affects:** Any app consuming `@clicktap/ui`'s `Select` / `DropdownSelect` (Crispi, FFL, and any fork that renders pickers, sort controls, country/region selects, or configurable-product options).
|
|
5
|
+
**Classification:** FEATURE + BREAKING
|
|
6
|
+
**Breaking:** Yes — (1) `DropdownSelect` removed, (2) the bare `<Select>` is now a **button picker**, not a combobox, (3) `DropdownSelect`'s `classNames.trigger` is now `classNames.input`, (4) the root is `w-full` by default, (5) `<Option>` is react-stately's `Item` — keyed by `key` (no `id`) and **cannot be wrapped**.
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
| Package | Minimum version |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `@clicktap/ui` | The release bundling this commit. Confirm with `jq '.dependencies["@clicktap/ui"]' apps/frontend/package.json`. |
|
|
13
|
+
|
|
14
|
+
(Actual version is set by semantic-release when `bugfixes` merges to `master`.)
|
|
15
|
+
|
|
16
|
+
## Summary
|
|
17
|
+
|
|
18
|
+
There used to be **two** components: `DropdownSelect` (a native-feel button picker) and `Select` (a react-aria `ComboBox` with a text input). They had different APIs, different `classNames` keys, and overlapping purposes.
|
|
19
|
+
|
|
20
|
+
They're now **one** component — `<Select>` — with a `searchable` prop:
|
|
21
|
+
|
|
22
|
+
- `searchable={false}` *(default)* — **button picker**: click to open, pick one. Right for short, fixed lists (sort order, "show per row"). Backed by a real hidden `<select>`, so it autofills.
|
|
23
|
+
- `searchable` *(`true`)* — **combobox**: type to filter; the first match highlights, Tab/Enter commit it and Tab advances to the next field. Right for long lists (country, region). The dropdown is **portaled** so it works inside modals/drawers, and it ships with a hidden `<select>` mirror so password managers / browser autofill still work.
|
|
24
|
+
- `searchable="auto"` — searchable once there are more than ~8 options.
|
|
25
|
+
|
|
26
|
+
`<Option>` is react-stately's `Item` (`OptionProps<T> = ItemProps<T>`). **This is a breaking change if you previously passed `id` or wrapped `<Option>`** — see [Options: `id` → `key`, do not wrap](#options-id--key-do-not-wrap).
|
|
27
|
+
|
|
28
|
+
## What changed
|
|
29
|
+
|
|
30
|
+
| Area | Before | After |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Exports | `DropdownSelect`, `DropdownSelectProps` + `Select` | `Select` only (`DropdownSelect` removed) |
|
|
33
|
+
| Bare `<Select>` | Combobox (type-to-filter) | **Button picker** — add `searchable` to keep filtering |
|
|
34
|
+
| `DropdownSelect` button class | `classNames.trigger` | `classNames.input` |
|
|
35
|
+
| Root width | `DropdownSelect`: auto (`flex items-center`) | `w-full` (form-field default) — override for inline/`justify-end` rows |
|
|
36
|
+
| **`<Option>`** | own element / accepted `id`, could be wrapped | **react-stately `Item`** — keyed by React `key` (no `id`); **cannot be wrapped** (it's a collection descriptor, not a rendered element). Style rows via `classNames.option`. |
|
|
37
|
+
| `SelectSlots.listBoxComponent` | Custom listbox renderer | **No-op** — the dropdown is rendered internally now; remove it |
|
|
38
|
+
| Option row theming | — | new `classNames.option` slot; focus/selected/disabled are `data-[focused]` / `data-[selected]` / `data-[disabled]` variants |
|
|
39
|
+
| Option groups | — | new `<Section title="…">` to group options (accessible heading); `classNames.sectionHeading` slot |
|
|
40
|
+
| Multi-select | — | `selectionMode="multiple"` → `selectedKeys: Key[]` + `onSelectionChange(keys: Key[])`. Searchable = combobox with chips; non-searchable = button with a summary. Single-select props/types unchanged. |
|
|
41
|
+
| Selection types | `SelectProps` is one interface | `SelectProps` is a **discriminated union** on `selectionMode`. New `SelectBaseProps` export = everything except the selection contract — base single-select **wrappers** on it (`Omit` over the union collapses to common keys). |
|
|
42
|
+
| New props | — | `searchable`, `selectTextOnFocus`, `autoFocusFirstOption`, `name` + `autoComplete` (autofill) |
|
|
43
|
+
| Filtering | name only | match by name **or** option `textValue` aliases (codes) |
|
|
44
|
+
|
|
45
|
+
Find affected usages:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
grep -rn 'DropdownSelect\|classNames=' apps/frontend libs/ui \
|
|
49
|
+
| grep -E 'DropdownSelect|trigger:|listBoxComponent' | head -40
|
|
50
|
+
# And every <Select> that relied on type-to-filter:
|
|
51
|
+
grep -rn '<Select' apps/frontend | grep -v 'searchable' | head -40
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Migration Steps
|
|
55
|
+
|
|
56
|
+
### 1. `DropdownSelect` → `<Select>` (default button mode)
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
// Before
|
|
60
|
+
import { DropdownSelect, Option } from '@clicktap/ui/components/Select';
|
|
61
|
+
|
|
62
|
+
<DropdownSelect
|
|
63
|
+
selectedKey={key}
|
|
64
|
+
onSelectionChange={onChange}
|
|
65
|
+
classNames={{ trigger: 'text-xs w-44 h-8', value: 'text-xs', label: '...' }}
|
|
66
|
+
>
|
|
67
|
+
<Option key="a">A</Option>
|
|
68
|
+
</DropdownSelect>
|
|
69
|
+
|
|
70
|
+
// After — same button look; `trigger` → `input`
|
|
71
|
+
import { Select, Option } from '@clicktap/ui/components/Select';
|
|
72
|
+
|
|
73
|
+
<Select
|
|
74
|
+
selectedKey={key}
|
|
75
|
+
onSelectionChange={onChange}
|
|
76
|
+
classNames={{ input: 'text-xs w-44 h-8', value: 'text-xs', label: '...' }}
|
|
77
|
+
>
|
|
78
|
+
<Option key="a">A</Option>
|
|
79
|
+
</Select>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Old `<Select>` that filtered → add `searchable`
|
|
83
|
+
|
|
84
|
+
The bare `<Select>` no longer renders a text input. If you relied on type-to-filter, opt in:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// Before — combobox behavior was implicit
|
|
88
|
+
<Select selectedKey={key} onSelectionChange={onChange}>{options}</Select>
|
|
89
|
+
|
|
90
|
+
// After — keep type-to-filter
|
|
91
|
+
<Select searchable selectedKey={key} onSelectionChange={onChange}>{options}</Select>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For long lists where you're unsure, `searchable="auto"` turns it on past ~8 options.
|
|
95
|
+
|
|
96
|
+
### 3. Fix inline / right-floated layout (the `w-full` root)
|
|
97
|
+
|
|
98
|
+
`DropdownSelect`'s root was auto-width; the new `Select` root is `w-full` (correct for form fields, wrong for a toolbar row). In a `justify-end` / `flex items-center` row it now fills the width and sits left. `cn` is `twMerge`, so a `className` width override on the **root** wins:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// Before (broke: fills the row, left-aligned)
|
|
102
|
+
<div className="flex items-center justify-end">
|
|
103
|
+
<Select classNames={{ input: 'w-44' }} ... />
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
// After (floats right again)
|
|
107
|
+
<div className="flex items-center justify-end">
|
|
108
|
+
<Select className="w-44" classNames={{ input: 'w-44' }} ... />
|
|
109
|
+
</div>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> `classNames.input` sizes the button/input; **`className` sizes the root.** Constrain the root anywhere the Select isn't meant to be full-width.
|
|
113
|
+
|
|
114
|
+
### 4. Options: `id` → `key`, do not wrap
|
|
115
|
+
|
|
116
|
+
`<Option>` is now react-stately's `Item` (`OptionProps<T> = ItemProps<T>`). Two consequences:
|
|
117
|
+
|
|
118
|
+
- **`id` is gone — options are keyed by the React `key`.** Every `<Option id={…}>` fails to typecheck.
|
|
119
|
+
```tsx
|
|
120
|
+
// Before
|
|
121
|
+
<Option id={country.code}>{country.name}</Option>
|
|
122
|
+
// After
|
|
123
|
+
<Option key={country.code}>{country.name}</Option>
|
|
124
|
+
```
|
|
125
|
+
- **You cannot wrap `<Option>`.** `Item` is a *collection descriptor*, not a rendered element — the Select reads its props to build the list and renders the rows itself. A wrapper (`<MyOption>` → styled `<div>` → `<Item>`) breaks twice: the React `key` is swallowed by the wrapper (identity lost) and the wrapper's markup/`className` is never rendered. Use `<Option>` **directly** and style rows with `classNames.option` (next step).
|
|
126
|
+
|
|
127
|
+
### 5. Theme the option rows (`classNames.option`)
|
|
128
|
+
|
|
129
|
+
Option rows previously hard-coded a light palette with no override hook. There's now a `classNames.option` slot, and the focus/selected/disabled states are emitted as **`data-*` variants** so a non-light theme can retarget them:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<Select
|
|
133
|
+
searchable
|
|
134
|
+
classNames={{
|
|
135
|
+
listContainer: 'bg-zinc-900 border-zinc-700', // the dropdown surface
|
|
136
|
+
option: 'text-zinc-100 data-[focused]:bg-zinc-800 data-[disabled]:text-zinc-500',
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{items.map((i) => <Option key={i.id}>{i.label}</Option>)}
|
|
140
|
+
</Select>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`classNames.option` is merged last, so `twMerge` lets it win over the defaults (`text-slate-900`, `data-[focused]:bg-slate-100`, …). This replaces the old descendant-selector hack (`classNames.list: '[&_li]:…'`) and any per-option wrapper.
|
|
144
|
+
|
|
145
|
+
### 6. Remove `slots.listBoxComponent`
|
|
146
|
+
|
|
147
|
+
The dropdown list is rendered internally now; a custom `listBoxComponent` is ignored. Delete it (style via `classNames.listContainer` / `classNames.list` / `classNames.option` instead).
|
|
148
|
+
|
|
149
|
+
### 7. (Optional) Enable autofill on a searchable select
|
|
150
|
+
|
|
151
|
+
A searchable combobox has no native form control for its *selection*, so password managers / browser autofill can't fill it — **unless** you pass `name` (and ideally an `autoComplete` token). The Select then renders a hidden native `<select>` (for ≤300 options) that autofill targets; its change drives the selection.
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
<Select
|
|
155
|
+
searchable
|
|
156
|
+
name="region"
|
|
157
|
+
autoComplete="address-level1" // "country" for a country field, etc.
|
|
158
|
+
selectedKey={value ?? null}
|
|
159
|
+
onSelectionChange={onChange}
|
|
160
|
+
>
|
|
161
|
+
{regions.map((r) => <Option key={r.code}>{r.name}</Option>)}
|
|
162
|
+
</Select>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Autofill matches the **option text** (the state/country name), so render the name as the option's children.
|
|
166
|
+
|
|
167
|
+
### 8. (Optional) Match by code/alias when typing
|
|
168
|
+
|
|
169
|
+
By default filtering matches the option's `textValue` (its display name). To let users type a **code** ("tx", "us-tx") and still find the row, put the aliases in `textValue` — the field still displays the rendered children, not `textValue`:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
<Option key={code} textValue={`${name} ${shortCode} ${code}`}>
|
|
173
|
+
{name}
|
|
174
|
+
</Option>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Match ranking.** While filtering a flat searchable list, matches are reordered
|
|
178
|
+
by relevance: an exact whitespace-token match (a code, e.g. `us`) first, then
|
|
179
|
+
options whose name **starts with** the query, then mid-word substring hits. So
|
|
180
|
+
typing `us` ranks *United States* (code `us`) above *Belarus* (substring), and
|
|
181
|
+
typing `uni` ranks *United States* (prefix) above *Reunion* (mid-word). Ranking
|
|
182
|
+
applies only when the Select owns filtering — it's skipped when you control
|
|
183
|
+
`inputValue` (you own ordering), pass a render-fn `children`, or use `<Section>`
|
|
184
|
+
groups (reordering would break grouping). See the `MatchRanking` story.
|
|
185
|
+
|
|
186
|
+
### 9. (Optional) Group options with `<Section>`
|
|
187
|
+
|
|
188
|
+
Wrap `<Option>`s in `<Section title="…">` for grouped lists (an accessible group
|
|
189
|
+
heading per section). Works in both modes; in searchable mode the filter matches
|
|
190
|
+
options within groups and drops empty ones, and Tab-complete commits the first
|
|
191
|
+
matching **option** (section headers are skipped). Style headings via
|
|
192
|
+
`classNames.sectionHeading`.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
import { Select, Option, Section } from '@clicktap/ui/components/Select';
|
|
196
|
+
|
|
197
|
+
<Select searchable classNames={{ sectionHeading: 'text-zinc-400' }}>
|
|
198
|
+
<Section title="North America">
|
|
199
|
+
<Option key="us">United States</Option>
|
|
200
|
+
<Option key="ca">Canada</Option>
|
|
201
|
+
</Section>
|
|
202
|
+
<Section title="Europe">
|
|
203
|
+
<Option key="fr">France</Option>
|
|
204
|
+
</Section>
|
|
205
|
+
</Select>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`<Section>` is react-stately's `Section` (a collection descriptor, like `<Option>`); it also accepts the dynamic `items` render form. `searchable="auto"` counts the options inside groups, not the groups.
|
|
209
|
+
|
|
210
|
+
### 10. (Optional) Multi-select
|
|
211
|
+
|
|
212
|
+
`selectionMode="multiple"` switches the selection contract to an array: pick
|
|
213
|
+
several options, the menu stays open until you click away. The callback receives
|
|
214
|
+
a `Key[]`. It works in **both** modes — searchable and not:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
const [keys, setKeys] = useState<Key[]>([]);
|
|
218
|
+
|
|
219
|
+
// Searchable → combobox multi: type to filter, picks shown as removable chips.
|
|
220
|
+
<Select searchable selectionMode="multiple" selectedKeys={keys} onSelectionChange={setKeys}>
|
|
221
|
+
{/* combines with <Section> groups too */}
|
|
222
|
+
</Select>
|
|
223
|
+
|
|
224
|
+
// Not searchable → button multi: a button whose summary lists the picks; the
|
|
225
|
+
// open listbox toggles each option (stays open).
|
|
226
|
+
<Select selectionMode="multiple" selectedKeys={keys} onSelectionChange={setKeys}>
|
|
227
|
+
{/* short fixed lists */}
|
|
228
|
+
</Select>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`SelectProps` is a **discriminated union** on `selectionMode`, so single-select
|
|
232
|
+
keeps its exact `selectedKey`/`onSelectionChange(Key | null)` types — no change to
|
|
233
|
+
existing call sites.
|
|
234
|
+
|
|
235
|
+
**Display & order conventions:**
|
|
236
|
+
- *Combobox (chips)* — `classNames.tagGroup` / `tag` / `tagRemoveButton`; chips in
|
|
237
|
+
**selection order** (newest last, the usual token-input convention); full
|
|
238
|
+
keyboard a11y via react-aria-components `TagGroup` (arrow-nav, Backspace/Delete).
|
|
239
|
+
- *Button (summary)* — comma-joined labels in **collection order** (stable, reads
|
|
240
|
+
like the list), styled via `classNames.value`.
|
|
241
|
+
|
|
242
|
+
> **Wrappers:** if you build a single-select wrapper that does
|
|
243
|
+
> `Omit<SelectProps, …>`, switch the base to the new **`SelectBaseProps`** export.
|
|
244
|
+
> `Omit` over a union keeps only keys common to *all* members, which silently
|
|
245
|
+
> drops the per-mode selection props. (This is why `AddressInput` was rebased.)
|
|
246
|
+
>
|
|
247
|
+
> _Caveats:_ button-mode multi has **no hidden native `<select>`** (so no
|
|
248
|
+
> native-form submission / autofill — consume via `selectedKeys`), and selected
|
|
249
|
+
> rows show via `data-[selected]` (no checkmark icon). A `tagOrder` toggle
|
|
250
|
+
> (collection-order chips) is deferred until a use case needs it.
|
|
251
|
+
|
|
252
|
+
### Verification
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
nx run frontend:typecheck # no errors referencing DropdownSelect / trigger / listBoxComponent
|
|
256
|
+
nx run ui:typecheck
|
|
257
|
+
nx run ui:build-storybook
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Visually confirm, per migrated select:
|
|
261
|
+
- [ ] Button selects open/select/close; value shows.
|
|
262
|
+
- [ ] Searchable selects: type to filter, Tab/Enter commit, Tab advances; clear + Tab clears.
|
|
263
|
+
- [ ] Searchable inside a **modal/drawer**: Tab moves to the next field (dropdown is portaled out of the focus trap).
|
|
264
|
+
- [ ] Toolbar/inline selects float/size correctly (root width set).
|
|
265
|
+
- [ ] Autofill (if enabled): password manager fills the field; value sticks.
|
|
266
|
+
- [ ] Themed rows (if you set `classNames.option`): text + focus/selected/disabled colors apply (see the `ThemedOptions` story).
|
|
267
|
+
- [ ] Grouped options (if you use `<Section>`): headings render, filtering matches within groups, Tab commits the first matching option (see the `Grouped` story).
|
|
268
|
+
- [ ] Multi-select (if you use `selectionMode="multiple"`): combobox shows chips (filter clears per pick, × removes); button shows a collection-order summary; menu stays open until click-away; callback gets `Key[]` (see `Multiple` / `GroupedMultiple` / `ButtonMultiple`).
|
|
269
|
+
|
|
270
|
+
## Rollback
|
|
271
|
+
|
|
272
|
+
Revert `@clicktap/ui` in the consuming app:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
docker compose exec -u app node-app pnpm --filter <app> add '@clicktap/ui@<previous-minor>'
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Safe — presentation-layer only, no persistent state.
|
|
279
|
+
|
|
280
|
+
## References
|
|
281
|
+
|
|
282
|
+
- Source: `libs/ui/src/components/Select/` (`Select.tsx` dispatches to `ButtonSelect.tsx` / `ComboBoxSelect.tsx`; shared parts in `parts.tsx`)
|
|
283
|
+
- Types: `libs/ui/src/components/Select/Select.types.ts`
|
|
284
|
+
- Stories: `libs/ui/src/components/Select/Select.stories.tsx` (`Picker`, `Searchable`, `SearchableAuto`, `AsyncDataSource`, `ServerSideSearch`, `ThemedOptions`, `Grouped`, `Multiple`, `GroupedMultiple`, `ButtonMultiple`)
|
|
285
|
+
- In-repo consumer examples: `apps/frontend/components/AddressForm/AddressForm.tsx` (country/region, autofill, aliases), `apps/frontend/components/Collection/CollectionToolbar.tsx` (toolbar width), `apps/frontend/components/Product/components/ConfigurableOptions/ConfigurableOptions.tsx`
|
|
286
|
+
- Related: [`Select` `onSelectionChange` widened to `Key | T`](../04/2026-04-17-select-address-input-onchange-key-or-item.md), [`Select` `popoverPortalContainer` restored](../02/2026-02-17-select-popover-portal-container-restored.md)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# `Select` (searchable): fix "setValue is not a function" crash on browser autofill
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-06-18
|
|
4
|
+
**Affects:** `@clicktap/ui` `Select` — the **searchable** (ComboBox) variant (`ComboBoxSelect`)
|
|
5
|
+
**Classification:** BUGFIX
|
|
6
|
+
**Breaking:** No
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
A **searchable** `<Select>` with a `name` (for autofill) crashed on **browser autofill** with:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
TypeError: setValue is not a function
|
|
14
|
+
at @react-aria/select .../HiddenSelect.mjs (its <select> onChange calls state.setValue)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`ComboBoxSelect` renders react-aria's `HiddenSelect` (a select-only helper) so password
|
|
18
|
+
managers / browser autofill have a native control to fill, and passed it the **combobox** state:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
<HiddenSelect state={state as unknown as SelectState<T>} … />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That cast is a lie at runtime: `HiddenSelect`'s `<select> onChange` calls `state.setValue`, which
|
|
25
|
+
exists on **SelectState** (`useSelectState`, the non-searchable `ButtonSelect`) but **not** on
|
|
26
|
+
**ComboBoxState** (`useComboBoxState`). So when autofill wrote into the hidden `<select>`, its change
|
|
27
|
+
handler threw. Manual entry didn't trigger it; autofilling an address/country/region/profile form
|
|
28
|
+
did. (`ButtonSelect` never crashed — it has a real `SelectState`.)
|
|
29
|
+
|
|
30
|
+
This was **not** dep-fixable — it reproduced on the latest `@clicktap/ui` + `react-aria`.
|
|
31
|
+
|
|
32
|
+
## The fix
|
|
33
|
+
|
|
34
|
+
Wrap the combobox state in a `Proxy` that supplies `setValue` (mapped to `setSelectedKey`) while
|
|
35
|
+
preserving the state's live getters (`selectedKey`, `collection`, …), and pass *that* to
|
|
36
|
+
`HiddenSelect`:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const hiddenSelectState = useMemo(
|
|
40
|
+
() =>
|
|
41
|
+
new Proxy(state, {
|
|
42
|
+
get(target, prop) {
|
|
43
|
+
if (prop === 'setValue') {
|
|
44
|
+
return (key: Key | null) => target.setSelectedKey(key);
|
|
45
|
+
}
|
|
46
|
+
return Reflect.get(target, prop);
|
|
47
|
+
},
|
|
48
|
+
}) as unknown as SelectState<T>,
|
|
49
|
+
[state],
|
|
50
|
+
);
|
|
51
|
+
// …
|
|
52
|
+
<HiddenSelect state={hiddenSelectState} … />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A `Proxy` (not a spread) is used so the combobox state's getters keep reflecting the current
|
|
56
|
+
selection. The hidden native `<select>` mirror — and thus autofill support — is retained; only the
|
|
57
|
+
crash is removed.
|
|
58
|
+
|
|
59
|
+
## Consumer action
|
|
60
|
+
|
|
61
|
+
If you applied an **interim workaround** while this was unfixed, you can remove it once on a build
|
|
62
|
+
with this fix (the next `@clicktap/ui` release, **> 0.40.1**):
|
|
63
|
+
|
|
64
|
+
- `searchable={false}` forced onto autofill-prone form selects (it routed to `ButtonSelect`/`SelectState` to dodge the crash, at the cost of type-to-filter) — restore `searchable`.
|
|
65
|
+
- a `pnpm patch @clicktap/ui` carrying the `setValue → setSelectedKey` shim — drop the patch.
|
|
66
|
+
|
|
67
|
+
No API change; searchable `Select`s with a `name` now survive autofill.
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- Dropping `name` does **not** avoid it — react-aria renders `HiddenSelect` regardless, so autofill
|
|
72
|
+
still reaches it.
|
|
73
|
+
- A larger future option is to drop `HiddenSelect` for the combobox entirely and use react-aria's
|
|
74
|
+
combobox form-value path (a hidden text input). The Proxy shim is the contained fix and keeps the
|
|
75
|
+
existing `<select>`-based autofill behavior.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `@clicktap/ui` resolves its deep subpaths under `moduleResolution: bundler` (CTAP-1060)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-07-31
|
|
4
|
+
**Package:** `@clicktap/ui`
|
|
5
|
+
**Breaking:** No — additive to the `exports` map. Existing `moduleResolution: node` / `node10`
|
|
6
|
+
consumers are unaffected (that resolver ignores `exports`); this only *adds* working resolution under
|
|
7
|
+
`bundler` / `node16` / `nodenext`.
|
|
8
|
+
**Classification:** BUGFIX (packaging) — unblocks `moduleResolution: bundler`, and with it TypeScript 7
|
|
9
|
+
|
|
10
|
+
## What was wrong
|
|
11
|
+
|
|
12
|
+
`@clicktap/ui`'s `exports` map pointed its wildcard subpaths at **extensionless** targets
|
|
13
|
+
(`"./utils/*": "./utils/*"`). The legacy `moduleResolution: node` resolver ignores `exports` and
|
|
14
|
+
probes files, and runtime bundlers append extensions, so this *appeared* to work — but
|
|
15
|
+
`moduleResolution: bundler` / `node16` / `nodenext` **use `exports` strictly** and require an
|
|
16
|
+
extensioned target. A deep import like `@clicktap/ui/utils/env` resolved to `./utils/env` (no such
|
|
17
|
+
file) → **~928 `Cannot find module` errors** the moment a consumer switched to `bundler`. TypeScript 7
|
|
18
|
+
removes `node10` outright (`TS5108`), forcing `bundler` / `node16` — so the old map made TS 7
|
|
19
|
+
unreachable for every consumer.
|
|
20
|
+
|
|
21
|
+
## What changed
|
|
22
|
+
|
|
23
|
+
The `exports` targets are now extensioned + `types`-conditioned, with an **array fallback** so both
|
|
24
|
+
flat files (`utils/cn.js`) and directory-index modules (`utils/variantOptions/index.js`) resolve:
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
"./utils/*": {
|
|
28
|
+
"types": ["./utils/*.d.ts", "./utils/*/index.d.ts"],
|
|
29
|
+
"import": ["./utils/*.js", "./utils/*/index.js"]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The same shape applies to `./components/*` and `./hooks/*`; `./types/*` is **type-only** (the build
|
|
34
|
+
emits only `.d.ts` for it).
|
|
35
|
+
|
|
36
|
+
## What to do
|
|
37
|
+
|
|
38
|
+
- **On `moduleResolution: node` today:** nothing — you are unaffected. Bump `@clicktap/ui` as usual.
|
|
39
|
+
- **Switching to `moduleResolution: bundler` / `node16`:** bump `@clicktap/ui` to ≥ this release and the
|
|
40
|
+
deep `@clicktap/ui/utils/*`, `/components/*`, `/hooks/*` imports resolve (types included). This was the
|
|
41
|
+
prerequisite that previously produced the wall of `Cannot find module` errors.
|
|
42
|
+
- **TypeScript 7:** this removes the `@clicktap/ui` blocker. Note TS 7 has a *separate*, non-clicktap
|
|
43
|
+
blocker — **nx 20.0.3 cannot build a project graph with TS 7 installed** (`Cannot read properties of
|
|
44
|
+
undefined (reading 'readFile')`) — so TS 7 also needs an nx major.
|
|
45
|
+
|
|
46
|
+
## Verified
|
|
47
|
+
|
|
48
|
+
Resolution was tested against the built package in both modes: under `bundler` all deep subpaths (flat
|
|
49
|
+
file, directory-index, and nested file) resolve **with types**; under `node` (node10) resolution is
|
|
50
|
+
unchanged (no regression).
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# NextUI → HeroUI for `Accordion`, `Badge`, `Pagination`, `Tooltip`
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-02
|
|
4
|
+
**Affects:** `@clicktap/ui` peer dependencies — `Accordion`, `Badge`, `Pagination`, `Tooltip`
|
|
5
|
+
**Classification:** DEPENDENCY
|
|
6
|
+
**Breaking:** Yes (peer dependencies change; requires Tailwind 4 — see below)
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
- **Requires Tailwind 4** (`@heroui/theme >= 2.4.24` peers `tailwindcss >= 4.0.0`). Do the
|
|
11
|
+
[Tailwind 3 → 4 upgrade](../../../../../apps/frontend/docs/migrations/2026/07/2026-07-31-tailwind-3-to-4.md)
|
|
12
|
+
first (or together with this).
|
|
13
|
+
|
|
14
|
+
## Summary
|
|
15
|
+
|
|
16
|
+
`@clicktap/ui`'s `Accordion`, `Badge`, `Pagination` and `Tooltip` now build on the **`@heroui/*`**
|
|
17
|
+
packages instead of the **deprecated** `@nextui-org/*` ones. npm reports each `@nextui-org/*` as
|
|
18
|
+
deprecated (`Please use @heroui/<name> instead`), and NextUI no longer receives fixes — they land only
|
|
19
|
+
in the HeroUI line. Consumers were inheriting four un-actionable `Deprecated` warnings through
|
|
20
|
+
`@clicktap/ui`; this removes them.
|
|
21
|
+
|
|
22
|
+
The component **APIs are unchanged** — HeroUI is a straight rename-fork of NextUI. No source changes are
|
|
23
|
+
needed at your call sites; this is a dependency swap.
|
|
24
|
+
|
|
25
|
+
## What changed in `@clicktap/ui`
|
|
26
|
+
|
|
27
|
+
| Before (peer) | After (peer) |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `@nextui-org/accordion` | `@heroui/accordion` |
|
|
30
|
+
| `@nextui-org/badge` | `@heroui/badge` |
|
|
31
|
+
| `@nextui-org/pagination` | `@heroui/pagination` |
|
|
32
|
+
| `@nextui-org/tooltip` | `@heroui/tooltip` |
|
|
33
|
+
| *(implicit / unpinned)* | `@heroui/theme` `>= 2.4.24` (declared) |
|
|
34
|
+
| *(implicit / unpinned)* | `@heroui/system` `>= 2.4.18` (declared) |
|
|
35
|
+
|
|
36
|
+
`@heroui/theme` / `@heroui/system` are now declared explicitly so their versions are aligned and
|
|
37
|
+
satisfiable (previously the theme/system peers came in transitively and could resolve to versions the
|
|
38
|
+
components rejected — e.g. `@heroui/theme@>=2.4.24` unmet with `2.4.6` installed).
|
|
39
|
+
|
|
40
|
+
## ⚠️ Tailwind 4 is required
|
|
41
|
+
|
|
42
|
+
`@heroui/theme >= 2.4.24` peers **`tailwindcss >= 4.0.0`**. The HeroUI theme line has moved to Tailwind
|
|
43
|
+
4, so the components' **styles** will not compile correctly under Tailwind 3. If you are still on
|
|
44
|
+
Tailwind 3, complete the Tailwind 3 → 4 upgrade first; the two migrations belong together. (This is why
|
|
45
|
+
`@clicktap/ui`'s own reference frontend, still on Tailwind 3, adopts this alongside its Tailwind 4 move.)
|
|
46
|
+
The type/build surface is fine on either — the coupling is purely the generated CSS.
|
|
47
|
+
|
|
48
|
+
## Migration steps (consumer)
|
|
49
|
+
|
|
50
|
+
1. **Bump `@clicktap/ui`** to the release that ships this migration.
|
|
51
|
+
2. **Replace the peer packages** in your `package.json` — remove the four `@nextui-org/*` above, add the
|
|
52
|
+
four `@heroui/*` plus `@heroui/theme` (`>= 2.4.24`) and `@heroui/system` (`>= 2.4.18`). If you wrap
|
|
53
|
+
these components, update any direct `@nextui-org/{accordion,badge,pagination,tooltip}` imports in your
|
|
54
|
+
own code to `@heroui/*` (identical API).
|
|
55
|
+
3. **Be on Tailwind 4** (see the dependency note). Point your HeroUI Tailwind plugin at `@heroui/theme`
|
|
56
|
+
instead of `@nextui-org/theme` if you configure it directly.
|
|
57
|
+
4. Reinstall and verify: no `Deprecated` warnings for these four, and no `unmet peer @heroui/theme|system`
|
|
58
|
+
warnings.
|
|
59
|
+
|
|
60
|
+
## Not included
|
|
61
|
+
|
|
62
|
+
- `@nextui-org/avatar` and the `@nextui-org/theme` Tailwind plugin still exist in the clicktap **reference
|
|
63
|
+
frontend** (not in `@clicktap/ui`); migrating those is separate and pairs with the Tailwind 4 work.
|
|
64
|
+
- This is unrelated to the CTAP-1072 SSR-500 (that was the `tailwindcss/plugin.js` async external, not
|
|
65
|
+
these packages).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# UI Library Migration Guides
|
|
2
|
+
|
|
3
|
+
Migration guides for the `@clicktap/ui` library. Each doc covers a specific change with before/after examples and checklists.
|
|
4
|
+
|
|
5
|
+
For migrations affecting the frontend app, see [frontend migrations](../../../../apps/frontend/docs/migrations/README.md).
|
|
6
|
+
|
|
7
|
+
## Index
|
|
8
|
+
|
|
9
|
+
| Date | Migration | Breaking | Dependencies |
|
|
10
|
+
|------|-----------|----------|--------------|
|
|
11
|
+
| 2026-01-14 | [External link detection + `LinkProvider`](./2026/01/2026-01-14-external-link-detection.md) | No | *None* |
|
|
12
|
+
| 2026-01-14 | [`useRoute` hook + `RouteProvider`](./2026/01/2026-01-14-use-route-hook.md) | No | *None* |
|
|
13
|
+
| 2026-01-15 | [`Input` `disableSkeleton` prop](./2026/01/2026-01-15-input-disable-skeleton.md) | No | *None* |
|
|
14
|
+
| 2026-02-16 | [Environment utilities — `@clicktap/ui/utils/env`](./2026/02/2026-02-16-env-utils-from-clicktap-ui.md) | No | *None* |
|
|
15
|
+
| 2026-02-17 | [`Select` `popoverPortalContainer` restored](./2026/02/2026-02-17-select-popover-portal-container-restored.md) | No | *None* |
|
|
16
|
+
| 2026-03-24 | [Image: dimmed, hidden, and onReady Props](./2026/03/2026-03-24-image-dimmed-hidden-onready.md) ↔ | No | *None* |
|
|
17
|
+
| 2026-03-24 | [ModalOverlay: fix dismiss on backdrop click](./2026/03/2026-03-24-modal-overlay-backdrop-dismiss.md) | No | *None* |
|
|
18
|
+
| 2026-04-17 | [`Select`/`AddressInput` `onChange` widened to `Key \| T`](./2026/04/2026-04-17-select-address-input-onchange-key-or-item.md) | Yes | *None* |
|
|
19
|
+
| 2026-04-24 | [`PLACEHOLDER_IMAGE` Export from `@clicktap/ui/utils/placeholder`](./2026/04/2026-04-24-image-placeholder-frontend-fallback.md) ↔ | No | *None* |
|
|
20
|
+
| 2026-04-30 | [Top-Down PDP Option Dependency — `@clicktap/ui/utils/variantOptions`](./2026/04/2026-04-30-top-down-pdp-option-dependency.md) ↔ | No | *None* |
|
|
21
|
+
| 2026-04-30 | [PDP Option Selection — Conservative Prune Helper (`pruneInvalidSelections`)](./2026/04/2026-04-30-prune-invalid-pdp-selections.md) ↔ | No | Top-Down PDP Option Dependency |
|
|
22
|
+
| 2026-05-01 | [`buildPdpUrl` Utility — `@clicktap/ui/utils/buildPdpUrl`](./2026/05/2026-05-01-build-pdp-url-util.md) | No | *None* |
|
|
23
|
+
| 2026-05-03 | [Auth-aware GraphQL client — `@clicktap/ui/utils/createAuthAwareGraphqlClient`](./2026/05/2026-05-03-auth-aware-graphql-client.md) | No | *None* |
|
|
24
|
+
| 2026-05-20 | [framer-motion 11.x idioms — opacity-only for modals, primitives elsewhere, matched keyframe shapes](./2026/05/2026-05-20-framer-motion-11-idioms.md) ↔ | Yes (custom `animationVariants` only) | *None* |
|
|
25
|
+
| 2026-05-21 | [`ButtonLoadingContent` helper — right-side spinner for all submit buttons](./2026/05/2026-05-21-button-loading-content.md) ↔ | No | *None* |
|
|
26
|
+
| 2026-05-21 | [`PasswordCheck` `requirements` variant hides until the visitor types](./2026/05/2026-05-21-password-check-hide-on-empty.md) | No | *None* |
|
|
27
|
+
| 2026-05-29 | [`PasswordCheck` — the `requirements` variant (adoption guide for forks)](./2026/05/2026-05-29-password-check-requirements-variant.md) | No | PasswordCheck hide-on-empty |
|
|
28
|
+
| 2026-06-17 | [Select standardization — one `<Select searchable>`, `DropdownSelect` removed](./2026/06/2026-06-17-select-standardization.md) ↔ | Yes | *None* |
|
|
29
|
+
| 2026-06-18 | [Select (searchable): fix "setValue is not a function" crash on browser autofill](./2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md) | No | Select standardization |
|
|
30
|
+
|
|
31
|
+
↔ = cross-app migration (has counterpart in frontend app)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clicktap/ui",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "Clicktap",
|
|
6
6
|
"description": "A library of React UI components and low-level hooks.",
|
|
@@ -76,10 +76,12 @@
|
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"@floating-ui/react": "^0.27.0",
|
|
79
|
-
"@
|
|
80
|
-
"@
|
|
81
|
-
"@
|
|
82
|
-
"@
|
|
79
|
+
"@heroui/accordion": "^2.2.0",
|
|
80
|
+
"@heroui/badge": "^2.2.0",
|
|
81
|
+
"@heroui/pagination": "^2.2.0",
|
|
82
|
+
"@heroui/system": ">=2.4.18",
|
|
83
|
+
"@heroui/theme": ">=2.4.24",
|
|
84
|
+
"@heroui/tooltip": "^2.2.0",
|
|
83
85
|
"@types/react": "^18.0.0",
|
|
84
86
|
"framer-motion": "^11.18.0",
|
|
85
87
|
"react": "^18.0.0",
|