@dloizides/ui-forms 1.16.0 → 1.18.0
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/CHANGELOG.md +402 -383
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +69 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +69 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,383 +1,402 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- `
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## 1.
|
|
23
|
-
|
|
24
|
-
**`
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
**Additive and backward-compatible.**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
is a
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
`
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
(`
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
-
|
|
262
|
-
`
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
-
|
|
269
|
-
|
|
270
|
-
-
|
|
271
|
-
|
|
272
|
-
`
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
- **`
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
- `
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.17.0
|
|
4
|
+
|
|
5
|
+
**Chips now have a 44px minimum touch target on web. `hitSlop` never did that — it is native-only.**
|
|
6
|
+
|
|
7
|
+
`CHIP_HIT_SLOP` was documented as expanding the chip's touch area toward the WCAG ≥44px floor
|
|
8
|
+
"without changing the rendered pill size". Measured in headless Chromium at 360x640 on 2026-09-21:
|
|
9
|
+
a chip's box is 33px tall (`paddingVertical: 6`), and `document.elementFromPoint(68, 35)` — 5px
|
|
10
|
+
above a top edge at y=40, inside `CHIP_HIT_SLOP.top: 10` — returns the PARENT element, not the
|
|
11
|
+
chip. react-native-web 0.21.2 references `hitSlop` only inside the legacy `Touchable` mixin's
|
|
12
|
+
press-RETENTION path (after the responder is already granted) and not at all in `Pressable`, so on
|
|
13
|
+
web the rendered box is the entire tappable area. The chips were a 33px target in all seven portals.
|
|
14
|
+
|
|
15
|
+
- `chipStyles.chip` and `chipStyles.chipOutline` gain `minHeight: 44` + `justifyContent: 'center'`.
|
|
16
|
+
Chip width is unchanged (56.7px and 68px before and after for the same labels), so wrapping and
|
|
17
|
+
`CHIP_GUTTER` spacing are untouched; a chip row grows 33px → 44px tall.
|
|
18
|
+
- `CHIP_HIT_SLOP` is kept and still helps iOS/Android. Its doc comment now states the web result
|
|
19
|
+
instead of claiming one.
|
|
20
|
+
- Consumers with a dense chip strip should re-check row spacing: the pill itself is taller now.
|
|
21
|
+
|
|
22
|
+
## 1.15.0
|
|
23
|
+
|
|
24
|
+
**`FormSwitch` now emits `aria-checked` / `aria-disabled` on web; `ChipSelector` gained an option icon slot and opt-in radio semantics.**
|
|
25
|
+
|
|
26
|
+
`FormSwitch` (A11Y-ARIA-CHECKED-1): react-native-web 0.21 emits nothing from `accessibilityState` on web, so the
|
|
27
|
+
switch wrapper rendered `role="switch"` with no `aria-checked` - an invalid switch that announced no state. The
|
|
28
|
+
state is now passed as literal `aria-checked` (always) and `aria-disabled` (only when disabled) web props;
|
|
29
|
+
`accessibilityState` also carries `disabled` for native.
|
|
30
|
+
|
|
31
|
+
`ChipSelector`:
|
|
32
|
+
- `ChipOption.icon?: React.ReactNode` - rendered before the label inside the chip, wrapped `aria-hidden`. Options
|
|
33
|
+
without an icon render the exact same tree as before.
|
|
34
|
+
- `singleSelectRole?: 'button' | 'radio'` (type `ChipSingleSelectRole`). Default `button` = unchanged. With
|
|
35
|
+
`radio` and `multiple` unset, the group is `role="radiogroup"` and each chip `role="radio"` + `aria-checked`.
|
|
36
|
+
Ignored for multi-select. Trade-off: RN-web renders a radio chip as a `<div>`, which activates on Enter but not
|
|
37
|
+
Space and has no arrow-key roving focus.
|
|
38
|
+
|
|
39
|
+
**Additive and backward-compatible.** No prop set = identical DOM for every existing consumer.
|
|
40
|
+
|
|
41
|
+
## 1.14.0
|
|
42
|
+
|
|
43
|
+
**`FormField` gained an opt-in `recentKey` — remember the last N submitted values and offer them on focus.**
|
|
44
|
+
|
|
45
|
+
A search / filter / lookup box that the user retypes the same handful of values into now gets a
|
|
46
|
+
"recent values" dropdown for free. Set `recentKey="<namespace>"` on a `FormField` and the values it
|
|
47
|
+
submits are remembered (per key) and surfaced in a menu on focus; picking one fills the input. A
|
|
48
|
+
value is recorded on submit (Enter) **and** on blur-with-content, so both "hit enter" and "typed then
|
|
49
|
+
moved on" are captured. The dropdown reuses the kit's `AnchoredMenu` (portalled to `document.body`, so
|
|
50
|
+
it is never painted behind a table / card / the next field) with full a11y (menu name, per-row label +
|
|
51
|
+
hint + role).
|
|
52
|
+
|
|
53
|
+
Persistence is `localStorage` under `@dloizides/ui-forms:recent:<recentKey>`; the list de-dupes
|
|
54
|
+
case-insensitively, promotes a repeat to the front, and caps at `recentMax` (default 10). On native /
|
|
55
|
+
SSR / when storage is disabled it is a silent no-op (recents are always `[]`), and every storage
|
|
56
|
+
access is wrapped so a quota or privacy-mode failure can never throw into render.
|
|
57
|
+
|
|
58
|
+
**Additive and backward-compatible.** Leave `recentKey` unset — the default — and the field behaves
|
|
59
|
+
exactly as before: nothing rendered, no state/refs/effects, no storage touched. NEVER set it on a
|
|
60
|
+
password / secure field. The engine is also exported standalone as `useRecentValues(key, max?)` for
|
|
61
|
+
bespoke recents surfaces, and `AnchoredMenu` gained an optional `accessibilityLabel` for naming the menu.
|
|
62
|
+
|
|
63
|
+
## 1.13.3
|
|
64
|
+
|
|
65
|
+
**Placeholder text was too dark — it read like an already-filled value.**
|
|
66
|
+
|
|
67
|
+
`useThemedInput` painted the placeholder with the theme's `textSecondary` — a deliberately
|
|
68
|
+
dark, accessible grey (design-tokens UX-11) that also renders readable secondary *copy*. At
|
|
69
|
+
full strength a placeholder (e.g. `follower` in a "Dance role" box) was hard to tell apart
|
|
70
|
+
from entered text. It is now `textSecondary` composited toward the field background at 0.6
|
|
71
|
+
alpha, so a hint reads clearly lighter than the solid entered text (`colors.text`) while
|
|
72
|
+
keeping the theme hue — and it works in dark mode too (it composites toward whatever the
|
|
73
|
+
field background is). The field's visible label still carries the accessible name, so this
|
|
74
|
+
is a usability win, not an a11y regression. Affects every `@dloizides/ui-forms`-driven input
|
|
75
|
+
(`FormField`, `Field`, `ThemedTextInput`, the `ui-tables` `Filters` search box) across all
|
|
76
|
+
seven portals.
|
|
77
|
+
|
|
78
|
+
## 1.12.0
|
|
79
|
+
|
|
80
|
+
**`FormField` gained `labelHidden` — hide the visible label, keep the accessible name.**
|
|
81
|
+
|
|
82
|
+
A field whose placeholder already names it (a toolbar search box, an inline filter) does
|
|
83
|
+
not want a stacked visible label: the label wastes a row and, worse, pushes the input a
|
|
84
|
+
label-row lower than a bare button beside it, so the two misalign. Until now the only way
|
|
85
|
+
to drop the visible label was to blank `label` — which ALSO blanked the input's
|
|
86
|
+
`accessibilityLabel`, silently stripping the accessible name.
|
|
87
|
+
|
|
88
|
+
`labelHidden` removes ONLY the visible `<label>` row: `label` stays required and the input
|
|
89
|
+
keeps `accessibilityLabel={label}`, so a screen reader reads the same name and the field
|
|
90
|
+
still resolves by role + name. Additive and backward-compatible — default `false`, every
|
|
91
|
+
existing call renders unchanged.
|
|
92
|
+
|
|
93
|
+
## 1.11.1
|
|
94
|
+
|
|
95
|
+
**`ThemedTextInput` shipped no metrics, so every direct caller got a raw 19px input.**
|
|
96
|
+
|
|
97
|
+
It documented itself as "the ONE source of truth for the input look" while shipping
|
|
98
|
+
only **colours** — no border, radius, padding or height. `FormField` quietly passed its
|
|
99
|
+
own private duplicate of those metrics, so anything reached *through* `FormField` looked
|
|
100
|
+
correct and the gap was invisible. Every **direct** caller — the documented, blessed
|
|
101
|
+
usage — got a raw browser input.
|
|
102
|
+
|
|
103
|
+
Measured live in the Digital Kin CMS before: **10/10 inputs below the 48px floor**, text
|
|
104
|
+
inputs 19px, textareas 38px. After: **0/10**, at 48/96px.
|
|
105
|
+
|
|
106
|
+
**Label association was absent entirely** (0 `label[for]`, 0 ids). Two defects only a
|
|
107
|
+
real browser could find, both fixed here:
|
|
108
|
+
|
|
109
|
+
- **`htmlFor` is silently dropped by react-native-web** — it is not on the prop
|
|
110
|
+
allowlist, the same family as the dropped `accessibilityHint`. A prop-level assertion
|
|
111
|
+
passes against the broken build; only a DOM-level test catches it, so that is what the
|
|
112
|
+
new tests assert.
|
|
113
|
+
- **Dangling `for`.** `Field` assumed render-function children place the id they are
|
|
114
|
+
handed, but `SelectControl` uses `aria-labelledby` instead — so the difficulty label
|
|
115
|
+
shipped `for="field-control-3"` pointing at **nothing**, passing every structural check
|
|
116
|
+
while focusing no field. `useHtmlFor` now sets `for` only when the target is genuinely
|
|
117
|
+
in the document.
|
|
118
|
+
|
|
119
|
+
> ⚠️ **Cross-fleet visual change.** `controlStyles.input` gaining `minHeight: 48` lifts
|
|
120
|
+
> dense filter inputs and select triggers in `aml-v2`/`agora-web` from ~38-41px to 48px.
|
|
121
|
+
> This cannot be avoided: the input floor is what fixes the 19px defect, and nothing
|
|
122
|
+
> fixes that without also lifting inputs that already had padding. Owner-approved.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## 1.9.0
|
|
126
|
+
|
|
127
|
+
**Campaign F3 — `Field`'s spacing model, and the layout primitives.** `Field` hard-coded a 16px
|
|
128
|
+
bottom margin on its container. That is the pre-flexbox model — the CHILD owns the space after
|
|
129
|
+
itself — and in a `gap`-laid-out parent, which is how every modern form row in this fleet is
|
|
130
|
+
written, the margin STACKS on the parent's gap. So consumers cancelled it by hand: **16
|
|
131
|
+
`containerStyle={{ marginBottom: 0 }}` sites across the fleet**, 13 of them in aml-v2, each with an
|
|
132
|
+
apologetic comment. AML v1 got this right in CSS years ago (`.ui-field { display: flex;
|
|
133
|
+
flex-direction: column; gap: 6px; }`) and wrote the contract down in `shared/FORMS.md`.
|
|
134
|
+
|
|
135
|
+
**This release moves NO pixels for any existing consumer.** The fix is opt-in, not a 2.0.0 — see
|
|
136
|
+
"Why not a major bump" below.
|
|
137
|
+
|
|
138
|
+
- **Add `FormGrid` + `FormCell`** — the wrapping form row, extracted from the copy the fleet
|
|
139
|
+
re-declares verbatim (`{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }` +
|
|
140
|
+
`{ minWidth: 220, flexGrow: 1, flexBasis: 260 }`), with the min-width/basis parameterised. Cell
|
|
141
|
+
metrics live on the GRID, so the common call site is a bare `<FormCell>` and only the genuine
|
|
142
|
+
one-offs (`<FormCell basis={160} grow={false} minWidth={140}>`) carry props.
|
|
143
|
+
- **Add `FormSection`** — the fieldset/legend grouping only v1 had. react-native-web has no
|
|
144
|
+
`fieldset` mapping, so this is the ARIA equivalent: `role="group"` + `aria-labelledby` pointing
|
|
145
|
+
at the legend. The legend reuses `resolveLabelVariantStyle` — the SAME function `Field` uses — so
|
|
146
|
+
a section heading cannot drift into a third label voice.
|
|
147
|
+
- **Add `Field.spacing` (`'stack' | 'gap'`)** — the two spacing models. `stack` is the historical
|
|
148
|
+
16px margin + 4px child margins, byte-identical to 1.8.0, and remains the default. `gap` is the
|
|
149
|
+
FORMS.md contract: no bottom margin, one uniform 6px internal gap, parent owns the rhythm.
|
|
150
|
+
- **`Field` inside a `FormGrid` or `FormSection` resolves to `gap` automatically.** Adopting the
|
|
151
|
+
shared grid IS the opt-in: a consumer deletes its hand-rolled `grid`/`field` styles AND its
|
|
152
|
+
`marginBottom: 0` cancel in the same edit. An explicit `spacing` prop beats the context in both
|
|
153
|
+
directions.
|
|
154
|
+
- **`ChipSelector`**: the chip row now spaces with `gap: 8` instead of per-chip margins plus a
|
|
155
|
+
compensating `marginBottom: -8`. Rendered box is byte-identical. Also gains a `spacing`
|
|
156
|
+
passthrough, and `containerStyle` widens to `StyleProp<ViewStyle>` to match `Field`'s.
|
|
157
|
+
|
|
158
|
+
### Why not a major bump
|
|
159
|
+
|
|
160
|
+
The brief for this work allowed a breaking 2.0.0. It was rejected. Switching every `Field` to the
|
|
161
|
+
gap model in one publish moves pixels on every form in seven portals simultaneously — which is
|
|
162
|
+
precisely the change you cannot visually QA, because no app remains on the old behaviour to compare
|
|
163
|
+
against, and a regression surfaces as "some form somewhere looks slightly wrong" with seven
|
|
164
|
+
suspects. The context-aware opt-in gives the identical end state via a per-app rollout, each step
|
|
165
|
+
behind visual QA, with `git revert` scoped to one app. The cost is that both models live in the
|
|
166
|
+
code until the fleet has migrated; that is a smaller price than an un-QA-able fleet-wide pixel
|
|
167
|
+
change.
|
|
168
|
+
|
|
169
|
+
### Correction to the campaign premise
|
|
170
|
+
|
|
171
|
+
`ChipSelector`'s `marginBottom: -CHIP_GUTTER` was documented — and briefed — as compensation for
|
|
172
|
+
`Field`'s 16px margin, load-bearing enough that changing one without the other would break chip
|
|
173
|
+
spacing in 7+ sites. **That was wrong.** The -8 cancelled the chips' OWN bottom gutter hanging off
|
|
174
|
+
the last wrapped row; it behaved identically whatever `Field` contributed. It has been replaced with
|
|
175
|
+
`gap` regardless, because a margin/negative-margin pair is fragile and leaks a negative margin onto
|
|
176
|
+
the block's box. `fieldSpacing.test.tsx` now pins `Field` and `ChipSelector` to the same box in both
|
|
177
|
+
models, so the relationship is checkable rather than folkloric.
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
## 1.8.0
|
|
181
|
+
|
|
182
|
+
**Campaign F2 — the dense CONTROLS arrive.** `@dloizides/ui-tables` shipped a complete, themed,
|
|
183
|
+
a11y-wired set of form controls inside its `Filters` bar and exported NONE of them: its
|
|
184
|
+
`src/index.ts` exposed only `Filters`, `useFilterDraft`, `suggestOptions`, `isUnmatched` and
|
|
185
|
+
types. Six portals could SEE those controls working and none could import them, so the fleet
|
|
186
|
+
reimplemented them — **6 independent select implementations, 5 date fields, 2 typeaheads**. Same
|
|
187
|
+
shape as `MultiTenancy.EntityFrameworkCore` shipping no EF Core. This release makes them public.
|
|
188
|
+
|
|
189
|
+
Nothing here moves a pixel for an existing consumer: the metrics are the promoted originals,
|
|
190
|
+
unchanged. `CONTAINER_MARGIN_BOTTOM` and `ChipSelector`'s compensating `marginBottom:
|
|
191
|
+
-CHIP_GUTTER` remain deliberately untouched.
|
|
192
|
+
|
|
193
|
+
- **Add `SelectControl`** — single-select enum/status dropdown. Bordered trigger + chevron opening
|
|
194
|
+
the shared `AnchoredMenu`.
|
|
195
|
+
- **Add `TypeaheadControl`** — search-as-you-type combobox (aml-v2's CountryPicker, generalized).
|
|
196
|
+
Free typing is preserved (the value is raw text the caller normalises at query time); picking a
|
|
197
|
+
suggestion fills the canonical LABEL, not the value.
|
|
198
|
+
- **Add `DateRangeControl`** — inclusive from/to pair. Editing one side patches ONLY that side.
|
|
199
|
+
- **Add `AnchoredMenu`** — the floating option list both of the above compose. Kept in-tree with
|
|
200
|
+
NO `ui-layout` dependency; that was a considered choice in `ui-tables` and it still holds, since
|
|
201
|
+
depending on the higher-level `ModalDropdown` would invert the graph for every consumer.
|
|
202
|
+
- **Add `controlStyles`** — the tuned metrics, promoted verbatim. `ui-tables`' `Filters/styles.ts`
|
|
203
|
+
said the quiet part out loud in its own header: it "reconcile[d] the per-field styles that were
|
|
204
|
+
copy-pasted across aml-v2's filter files into one shared source" — and then locked the
|
|
205
|
+
reconciliation inside one bar where no other surface could reach it.
|
|
206
|
+
- **Add `suggestOptions` / `isUnmatched`** — the ranked substring matcher behind the typeahead.
|
|
207
|
+
- **Add `ControlOption` / `DateRangeValue`** types. `ui-tables` now ALIASES its long-standing
|
|
208
|
+
public `FilterOption` / `DateRangeValue` onto these, so there is one definition rather than two
|
|
209
|
+
structurally-compatible twins free to drift.
|
|
210
|
+
|
|
211
|
+
### The controls are LABEL-FREE, on purpose
|
|
212
|
+
|
|
213
|
+
They render the control only. Compose them inside `Field` — with `labelVariant="control"` on a
|
|
214
|
+
dense surface — to get the label, hint and error slots. That is what retires `ui-tables`' private
|
|
215
|
+
`FieldShell` fork (see `@dloizides/ui-tables` 1.14.0) and leaves ONE label implementation for
|
|
216
|
+
every control instead of one per control.
|
|
217
|
+
|
|
218
|
+
### i18n: this package still never calls `t`
|
|
219
|
+
|
|
220
|
+
Every string — `accessibilityLabel`, `placeholder`, `optionHint`, `error`, `fromLabel`/`toLabel`
|
|
221
|
+
— is a PRE-LOCALIZED prop. This is load-bearing, not stylistic: aml-v2 localizes through
|
|
222
|
+
`@dloizides/i18n` with positional `{0}` while the other six portals use i18next `{{p1}}`, so a
|
|
223
|
+
component that localized internally could not serve both. `SelectControl`'s `accessibilityLabel`
|
|
224
|
+
is therefore composed BY THE CALLER — it must carry the selection ("Status: Active", not
|
|
225
|
+
"Status"), and building that string needs a `t` call this package must not make.
|
|
226
|
+
|
|
227
|
+
### Fixed — a WCAG defect carried in by the promoted code
|
|
228
|
+
|
|
229
|
+
`AnchoredMenu` marked the current option with `accessibilityState={{ selected }}` alone.
|
|
230
|
+
**react-native-web does not read `accessibilityState` at all** (verified against
|
|
231
|
+
`react-native-web@0.21`'s `createDOMProps`, which enumerates `aria-selected` /
|
|
232
|
+
`accessibilitySelected` and never `accessibilityState`), so on web the selected option was
|
|
233
|
+
distinguished by colour and font-weight ONLY — no screen reader could tell which option was
|
|
234
|
+
current (WCAG 1.4.1, use of colour). `aria-selected` is now passed alongside, so each platform
|
|
235
|
+
gets the channel it honours. The trigger already paired `aria-expanded` with `accessibilityState`;
|
|
236
|
+
the option row was simply missed when that was done. A test asserts it, and that test FAILED
|
|
237
|
+
before the fix.
|
|
238
|
+
|
|
239
|
+
## 1.7.0
|
|
240
|
+
|
|
241
|
+
Additive wave implementing the shared cross-stack forms contract now written down at
|
|
242
|
+
`PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md`. **Nothing in this release moves a pixel
|
|
243
|
+
for an existing consumer**: every addition is opt-in and every default is the previous behaviour.
|
|
244
|
+
`CONTAINER_MARGIN_BOTTOM` and `ChipSelector`'s compensating `marginBottom: -CHIP_GUTTER` are
|
|
245
|
+
deliberately untouched — they are load-bearing and move together in a later, visual-QA-gated wave.
|
|
246
|
+
|
|
247
|
+
- **Add `FormActions` — the submit/cancel row.** Promoted from the BYTE-IDENTICAL
|
|
248
|
+
`erevna-web/src/components/Forms/FormActions.tsx` and `katalogos-web/.../FormActions.tsx`
|
|
249
|
+
(68 lines each; `diff` returns nothing). All seven portals hand-roll this row; aml-v2, agora,
|
|
250
|
+
kefi, ichnos and zygos each inline their own variant. The primary action is LAST in DOM order so
|
|
251
|
+
tab reaches Cancel before Save, and `saving` drives the primary button's spinner rather than
|
|
252
|
+
disabling-and-relabelling it. `saveDisabled` blocks Save WITHOUT blocking Cancel — an invalid
|
|
253
|
+
form stays escapable. testIDs default to the twins' `save-button` / `cancel-button`, so existing
|
|
254
|
+
Playwright selectors keep matching.
|
|
255
|
+
- **`Field` gains `hint`.** ~21 hand-rolled copies of `{fontSize:12, marginTop:4, color:
|
|
256
|
+
textSecondary}` exist across the fleet; aml-v2 alone declares a local `hint` style in 8 files.
|
|
257
|
+
Because `Field` had no hint slot, consumers rendered the hint as a SIBLING, where it lands past
|
|
258
|
+
the container's 16px bottom margin and detaches from the control it explains (see the apologetic
|
|
259
|
+
comment in `aml-v2/src/screens/screening/CoverageSelector.tsx`). The hint renders UNDER the
|
|
260
|
+
control and ABOVE the error, and hint and error COEXIST — both are reachable from the control via
|
|
261
|
+
a composed `aria-describedby` (hint id then error id, in reading order). Only the error is a
|
|
262
|
+
`role="alert"` live region; a hint that announced itself would interrupt on every focus.
|
|
263
|
+
- **`Field` gains `optional` + `optionalLabel`.** Mutually exclusive with `required`; when a caller
|
|
264
|
+
sets both, `required` WINS — the safe direction, since an under-marked required field sends the
|
|
265
|
+
user into a failed submit. Enforced at runtime in `resolveFieldMarker` rather than in the type
|
|
266
|
+
system, so a caller spreading a computed props object still gets a defined single-marker result
|
|
267
|
+
instead of a compile error it cannot act on. Like the asterisk, the optional text is decorative
|
|
268
|
+
(`aria-hidden`) — "optional" is the ABSENCE of `aria-required`, which assistive tech already
|
|
269
|
+
conveys.
|
|
270
|
+
- **`Field` gains `labelVariant` (+ `labelStyle` as the escape hatch).** `Field` hard-coded
|
|
271
|
+
13/600/sentence-case, which is exactly why kefi-web refused it and why `@dloizides/ui-tables`'
|
|
272
|
+
private `FieldShell` forked to 11/700/uppercase. `labelVariant` makes the contract's TWO
|
|
273
|
+
legitimate voices first-class and named: `field` (13/600 sentence case — a label that is READ)
|
|
274
|
+
and `control` (11/700 UPPERCASE + letter-spacing — a label that is SCANNED, for dense filter bars
|
|
275
|
+
and toolbars). An absent variant resolves to `field`, so every existing consumer renders
|
|
276
|
+
byte-identically. `labelStyle` applies AFTER the variant for the genuine one-off.
|
|
277
|
+
- **`Field`'s `containerStyle` widens `ViewStyle` -> `StyleProp<ViewStyle>`.** The narrow type was a
|
|
278
|
+
documented pain: `aml-v2/src/screens/leaders/CountryPicker.tsx` has to build a COMPLETE style per
|
|
279
|
+
variant because it cannot compose an array.
|
|
280
|
+
- **`useThemedInput`: a focused errored field now keeps a RED focus ring.** It previously computed
|
|
281
|
+
`isFocused && !hasError`, so an errored field got NO focus ring at all — removing the focus
|
|
282
|
+
indicator (WCAG 2.4.7) from precisely the field the user was sent back to fix, at the moment they
|
|
283
|
+
are most likely navigating by keyboard. It now always rings on focus and only changes the ring's
|
|
284
|
+
COLOUR, matching v1's `input.invalid:focus { box-shadow: 0 0 0 3px var(--danger-tint) }`.
|
|
285
|
+
One pre-existing test necessarily changed: it asserted `boxShadow === 'none'` for this state,
|
|
286
|
+
i.e. it encoded the defect as intended behaviour.
|
|
287
|
+
- **New peer dependency `@dloizides/ui-buttons >= 1.4.0`**, required by `FormActions` so the estate
|
|
288
|
+
keeps exactly one button implementation rather than forking one here.
|
|
289
|
+
- No i18n runtime is introduced: all user-visible copy (`saveLabel`, `cancelLabel`, `optionalLabel`,
|
|
290
|
+
`hint`, `error`) arrives pre-localized. aml-v2 localizes via `@dloizides/i18n` with positional
|
|
291
|
+
`{0}` params while the other six apps use i18next `{{p1}}`, and nothing bridges them, so a
|
|
292
|
+
component that localized internally could not serve both.
|
|
293
|
+
- Also shipping in this tarball, from a CONCURRENT workstream (commit `13bd281`, not part of this
|
|
294
|
+
wave): `ChipSelector` gains `chipTestIDPrefix`, `optionAccessibilityHint` and `multiple`
|
|
295
|
+
toggle semantics, and reads its on-primary ink from the theme's `onBrand` scale instead of a
|
|
296
|
+
hardcoded `#fff`. All opt-in; the `chip-selector-chip-<value>` testID default is frozen.
|
|
297
|
+
|
|
298
|
+
## 1.6.0
|
|
299
|
+
|
|
300
|
+
- **`ChipSelector` now composes `Field` internally — its label was drifting.** It rendered its OWN
|
|
301
|
+
`<Text>` label whose metrics differed from `Field`'s on three of four axes: `marginBottom: 8`
|
|
302
|
+
(vs 4), NO `fontSize` at all so it fell back to RN's ~14 (vs 13), and `colors.text`
|
|
303
|
+
(vs `textSecondary`). A `ChipSelector` next to a `FormField` in a row/grid therefore started a
|
|
304
|
+
different distance down the column and read as a different weight of text. Found at 4 sites in
|
|
305
|
+
aml-v2 (`TenantWatchlistsEditor`, `CoverageSelector`, `CustomCoverageFields`,
|
|
306
|
+
`AdvancedMatching`); fixing it in the package fixes every consumer with zero app churn.
|
|
307
|
+
The local label styles are deleted — there is now exactly one label implementation in the kit.
|
|
308
|
+
- **`ChipSelectorProps` is unchanged and fully backward-compatible** — every existing prop keeps
|
|
309
|
+
its name, type and default, and `label` stays OPTIONAL. With no label (or an empty one) NO label
|
|
310
|
+
row is rendered, so composing `Field` cannot introduce a phantom gap above the chips.
|
|
311
|
+
- **`ChipSelector` gains `required`, `error` and `testID`** (all optional, all additive) now that
|
|
312
|
+
`Field` provides them for free. `error` renders the same `role="alert"` line as `FormField` and
|
|
313
|
+
is tied to the chip group via `aria-describedby` + `aria-invalid` (web only; no-op on native).
|
|
314
|
+
- **Spacing:** a `ChipSelector` now carries `Field`'s `marginBottom: 16` like every other field
|
|
315
|
+
(it previously had none). The chips' own 8px trailing gutter is cancelled on the group so the
|
|
316
|
+
block's total bottom spacing is exactly 16 — the same as a `FormField`, not 24. Pass
|
|
317
|
+
`containerStyle` to override, as before.
|
|
318
|
+
- **`Field`'s `label` is now optional** (widened from `label: string`, so existing callers are
|
|
319
|
+
unaffected). An absent or empty label renders no label row, letting a control whose label lives
|
|
320
|
+
elsewhere still use the shared shell for its error line and spacing.
|
|
321
|
+
- `FormSwitch` / `FormCheckbox` are deliberately NOT changed — see `src/labelRoles.test.tsx`.
|
|
322
|
+
Their labels are INLINE control labels (beside the control in a row, the row's primary text),
|
|
323
|
+
not field headers stacked above a control, so they have no alignment defect to fix and `Field`'s
|
|
324
|
+
smaller/dimmer header treatment would be a regression. They were already identical to each
|
|
325
|
+
other; a new test now enforces that they stay so.
|
|
326
|
+
|
|
327
|
+
## 1.5.0
|
|
328
|
+
|
|
329
|
+
- **Add `Field` — the generic label-over-control wrapper.** `FormField` is hard-wired to a text
|
|
330
|
+
input (`FormFieldProps extends Omit<TextInputProps,'style'>`), so any NON-text control (a
|
|
331
|
+
dropdown, a date picker, a chip selector) had no shared way to get a label — and ended up
|
|
332
|
+
visually misaligned next to its `FormField` siblings: the control's box started ~21px higher
|
|
333
|
+
(no label row) and it missed `FormField`'s `marginBottom: 16`, so wrapped rows lost their
|
|
334
|
+
vertical rhythm. This was hand-rolled in 5 places across zygos-web and aml-v2.
|
|
335
|
+
`Field` takes `{ label, required?, error?, children, containerStyle?, testID? }` and renders the
|
|
336
|
+
same label row / required mark / error line / spacing.
|
|
337
|
+
- **`FormField` now composes `Field` internally**, so the two can never drift apart — the metrics
|
|
338
|
+
live in exactly one place. `FormFieldProps` and every rendered attribute (`form-field-input`
|
|
339
|
+
testID, `aria-invalid` / `aria-describedby` / `aria-required`, the `role="alert"` error line)
|
|
340
|
+
are unchanged: fully backward-compatible.
|
|
341
|
+
- `Field`'s `children` may be a plain node (the common case) or a render function receiving
|
|
342
|
+
`{ describedById, hasError }`, so a custom control can wire the error line into its own
|
|
343
|
+
`aria-describedby` / invalid state exactly the way `FormField` does.
|
|
344
|
+
|
|
345
|
+
## 1.4.0
|
|
346
|
+
|
|
347
|
+
- **Accessibility (WCAG 2.1 AA) hardening — additive + backward-compatible.**
|
|
348
|
+
- `FormField`: the error message is now programmatically tied to the input — the input gets
|
|
349
|
+
`aria-invalid` when errored and `aria-describedby` pointing at the error line (which is a
|
|
350
|
+
`role="alert"` live region), and `aria-required` when `required`. The required `*` is marked
|
|
351
|
+
decorative (`aria-hidden` / `accessibilityElementsHidden`) so a screen reader hears "required"
|
|
352
|
+
(via `aria-required`) instead of "star".
|
|
353
|
+
- `ThemedTextInput`: new optional `describedById` + `requiredField` props forward
|
|
354
|
+
`aria-describedby` / `aria-required`, and it emits `aria-invalid` from `hasError` (web only;
|
|
355
|
+
no-ops on native). Existing callers are unaffected.
|
|
356
|
+
- `FormCheckbox`: now keyboard-operable — the `role="checkbox"` Pressable renders a `<div>`
|
|
357
|
+
(no free Enter/Space), so an explicit web-only Enter/Space handler toggles it (WCAG 2.1.1).
|
|
358
|
+
- `ChipSelector`: chips gain a `hitSlop` so the touch target reaches ~44px WITHOUT changing the
|
|
359
|
+
rendered pill size, and expose `accessibilityState.disabled` when the selector is disabled.
|
|
360
|
+
- `useThemedInput`: the focus/hover CSS transition now collapses to instant when the user
|
|
361
|
+
requests `prefers-reduced-motion: reduce` (WCAG 2.3.3, web only).
|
|
362
|
+
|
|
363
|
+
## 1.3.0
|
|
364
|
+
|
|
365
|
+
- `ChipSelector` gains a `variant` prop (`'solid' | 'outline'`, default `'solid'`). `solid` is the
|
|
366
|
+
original filled-pill look (selected = solid brand fill, white ink) — existing consumers
|
|
367
|
+
(erevna / katalogos / kefi) render **identically**, no change. `outline` is the AML v1 console
|
|
368
|
+
look: an outlined pill on a muted fill where the **selected** state is a subtle tinted-outline
|
|
369
|
+
(brand border + brand ink + a low-alpha brand wash), NOT a solid fill; on web, hover lifts the
|
|
370
|
+
border + ink to the brand colour. Fully theme-driven (brand colour + alpha, border/text from the
|
|
371
|
+
theme), so it re-themes per tenant. Additive + backward-compatible.
|
|
372
|
+
|
|
373
|
+
## 1.2.0
|
|
374
|
+
|
|
375
|
+
- Add `ThemedTextInput` + `useThemedInput` — the shared "readable + lively" text-input treatment,
|
|
376
|
+
driven entirely by the active theme (re-themes per tenant, no hard-coded hex). At rest: dark
|
|
377
|
+
readable text on a subtle off-white surface with a soft border. On hover (web): the border
|
|
378
|
+
strengthens a touch. On focus: the border becomes the brand primary, the background brightens to
|
|
379
|
+
the plain surface (white), and a soft 3px brand-tinted focus ring fades in. The animation is a
|
|
380
|
+
pure GPU-cheap CSS transition on `border-color`/`background-color`/`box-shadow` (the ring is a
|
|
381
|
+
`box-shadow`, so no layout/reflow) — NO `Animated`/`LayoutAnimation`/JS timers. Web-only style
|
|
382
|
+
keys and the mouse-hover handlers are guarded behind `Platform.OS === 'web'`, so it stays
|
|
383
|
+
native-safe (native applies the state-driven colours instantly).
|
|
384
|
+
- `FormField` now renders `ThemedTextInput`, so every field gets the treatment for free; its label
|
|
385
|
+
uses `textSecondary` at 13px/600 for readability. Props/behaviour (label, hint, error, testID,
|
|
386
|
+
a11y, `onFocus`/`onBlur` pass-through) are backward-compatible.
|
|
387
|
+
|
|
388
|
+
## 1.1.0
|
|
389
|
+
|
|
390
|
+
- Add `FormCheckbox` — a native-safe (`Pressable`-based, RN-web + native) themed checkbox: a rounded
|
|
391
|
+
square that fills with the brand colour and shows a check glyph when on, with a label and optional
|
|
392
|
+
hint. Mirrors `FormSwitch`'s prop shape (`value`, `onValueChange`/`onChange`, `label`, `hint`,
|
|
393
|
+
`disabled`, `testID`, a11y) so it is a drop-in for boolean toggles that should read as checkboxes.
|
|
394
|
+
Exposes `role="checkbox"` + `aria-checked` / `accessibilityState={{ checked }}`.
|
|
395
|
+
|
|
396
|
+
## 1.0.0
|
|
397
|
+
|
|
398
|
+
Initial release. Extracted the proven duplicated form components from erevna-web ↔ katalogos-web
|
|
399
|
+
(Capability Wave C1, batch 2).
|
|
400
|
+
|
|
401
|
+
- `FormField`, `ChipSelector`, `FormSwitch` — share the `@dloizides/ui-feedback` UI context (`useUi`).
|
|
402
|
+
- `FormActions` deferred (depends on a not-yet-shared `core/Button`).
|