@dloizides/ui-forms 1.8.0 → 1.9.1
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 +55 -0
- package/dist/index.d.mts +176 -4
- package/dist/index.d.ts +176 -4
- package/dist/index.js +205 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +203 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
**Campaign F3 — `Field`'s spacing model, and the layout primitives.** `Field` hard-coded a 16px
|
|
6
|
+
bottom margin on its container. That is the pre-flexbox model — the CHILD owns the space after
|
|
7
|
+
itself — and in a `gap`-laid-out parent, which is how every modern form row in this fleet is
|
|
8
|
+
written, the margin STACKS on the parent's gap. So consumers cancelled it by hand: **16
|
|
9
|
+
`containerStyle={{ marginBottom: 0 }}` sites across the fleet**, 13 of them in aml-v2, each with an
|
|
10
|
+
apologetic comment. AML v1 got this right in CSS years ago (`.ui-field { display: flex;
|
|
11
|
+
flex-direction: column; gap: 6px; }`) and wrote the contract down in `shared/FORMS.md`.
|
|
12
|
+
|
|
13
|
+
**This release moves NO pixels for any existing consumer.** The fix is opt-in, not a 2.0.0 — see
|
|
14
|
+
"Why not a major bump" below.
|
|
15
|
+
|
|
16
|
+
- **Add `FormGrid` + `FormCell`** — the wrapping form row, extracted from the copy the fleet
|
|
17
|
+
re-declares verbatim (`{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }` +
|
|
18
|
+
`{ minWidth: 220, flexGrow: 1, flexBasis: 260 }`), with the min-width/basis parameterised. Cell
|
|
19
|
+
metrics live on the GRID, so the common call site is a bare `<FormCell>` and only the genuine
|
|
20
|
+
one-offs (`<FormCell basis={160} grow={false} minWidth={140}>`) carry props.
|
|
21
|
+
- **Add `FormSection`** — the fieldset/legend grouping only v1 had. react-native-web has no
|
|
22
|
+
`fieldset` mapping, so this is the ARIA equivalent: `role="group"` + `aria-labelledby` pointing
|
|
23
|
+
at the legend. The legend reuses `resolveLabelVariantStyle` — the SAME function `Field` uses — so
|
|
24
|
+
a section heading cannot drift into a third label voice.
|
|
25
|
+
- **Add `Field.spacing` (`'stack' | 'gap'`)** — the two spacing models. `stack` is the historical
|
|
26
|
+
16px margin + 4px child margins, byte-identical to 1.8.0, and remains the default. `gap` is the
|
|
27
|
+
FORMS.md contract: no bottom margin, one uniform 6px internal gap, parent owns the rhythm.
|
|
28
|
+
- **`Field` inside a `FormGrid` or `FormSection` resolves to `gap` automatically.** Adopting the
|
|
29
|
+
shared grid IS the opt-in: a consumer deletes its hand-rolled `grid`/`field` styles AND its
|
|
30
|
+
`marginBottom: 0` cancel in the same edit. An explicit `spacing` prop beats the context in both
|
|
31
|
+
directions.
|
|
32
|
+
- **`ChipSelector`**: the chip row now spaces with `gap: 8` instead of per-chip margins plus a
|
|
33
|
+
compensating `marginBottom: -8`. Rendered box is byte-identical. Also gains a `spacing`
|
|
34
|
+
passthrough, and `containerStyle` widens to `StyleProp<ViewStyle>` to match `Field`'s.
|
|
35
|
+
|
|
36
|
+
### Why not a major bump
|
|
37
|
+
|
|
38
|
+
The brief for this work allowed a breaking 2.0.0. It was rejected. Switching every `Field` to the
|
|
39
|
+
gap model in one publish moves pixels on every form in seven portals simultaneously — which is
|
|
40
|
+
precisely the change you cannot visually QA, because no app remains on the old behaviour to compare
|
|
41
|
+
against, and a regression surfaces as "some form somewhere looks slightly wrong" with seven
|
|
42
|
+
suspects. The context-aware opt-in gives the identical end state via a per-app rollout, each step
|
|
43
|
+
behind visual QA, with `git revert` scoped to one app. The cost is that both models live in the
|
|
44
|
+
code until the fleet has migrated; that is a smaller price than an un-QA-able fleet-wide pixel
|
|
45
|
+
change.
|
|
46
|
+
|
|
47
|
+
### Correction to the campaign premise
|
|
48
|
+
|
|
49
|
+
`ChipSelector`'s `marginBottom: -CHIP_GUTTER` was documented — and briefed — as compensation for
|
|
50
|
+
`Field`'s 16px margin, load-bearing enough that changing one without the other would break chip
|
|
51
|
+
spacing in 7+ sites. **That was wrong.** The -8 cancelled the chips' OWN bottom gutter hanging off
|
|
52
|
+
the last wrapped row; it behaved identically whatever `Field` contributed. It has been replaced with
|
|
53
|
+
`gap` regardless, because a margin/negative-margin pair is fragile and leaks a negative margin onto
|
|
54
|
+
the block's box. `fieldSpacing.test.tsx` now pins `Field` and `ChipSelector` to the same box in both
|
|
55
|
+
models, so the relationship is checkable rather than folkloric.
|
|
56
|
+
|
|
57
|
+
|
|
3
58
|
## 1.8.0
|
|
4
59
|
|
|
5
60
|
**Campaign F2 — the dense CONTROLS arrive.** `@dloizides/ui-tables` shipped a complete, themed,
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { StyleProp, TextStyle, ViewStyle, TextInputProps, TextInput, View } from 'react-native';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* `Field`'s TWO spacing models — the F3 fix, shipped as an OPT-IN rather than a breaking change.
|
|
6
|
+
*
|
|
7
|
+
* ## The defect
|
|
8
|
+
*
|
|
9
|
+
* `Field` hard-coded a 16px bottom margin on its container and 4px margins on its label / hint /
|
|
10
|
+
* error. That is the pre-flexbox model: the CHILD owns the space after itself. In a `gap`-laid-out
|
|
11
|
+
* parent — which is how every modern form row in this fleet is written — the child's margin STACKS
|
|
12
|
+
* on the parent's gap, so the consumer has to cancel it by hand. aml-v2 alone carries 13
|
|
13
|
+
* `marginBottom: 0` cancels, each with an apologetic comment; the fleet carries 16.
|
|
14
|
+
*
|
|
15
|
+
* AML v1 got this right in CSS: `.ui-field { display: flex; flex-direction: column; gap: 6px; }` —
|
|
16
|
+
* internal gap only, and the parent grid owns the space BETWEEN fields. That contract is written up
|
|
17
|
+
* in `PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md` ("The gap is owned by the container,
|
|
18
|
+
* never by margins on the children").
|
|
19
|
+
*
|
|
20
|
+
* ## Why opt-in and not a 2.0.0
|
|
21
|
+
*
|
|
22
|
+
* Switching every `Field` to the gap model at once moves pixels on every form in seven portals in a
|
|
23
|
+
* single publish — which is exactly the change you cannot visually QA, because there is no app you
|
|
24
|
+
* can look at that is still on the old behaviour to compare against. The two models are therefore
|
|
25
|
+
* both implemented, selected per field:
|
|
26
|
+
*
|
|
27
|
+
* - `stack` (DEFAULT) — the historical margins, byte-identical to 1.8.0. Every existing consumer
|
|
28
|
+
* is untouched, including the 16 that currently cancel the margin.
|
|
29
|
+
* - `gap` — the FORMS.md contract: no bottom margin at all, uniform 6px internal gap. The parent
|
|
30
|
+
* owns the rhythm.
|
|
31
|
+
*
|
|
32
|
+
* A `Field` inside a `FormGrid` resolves to `gap` automatically, so adopting the shared grid IS the
|
|
33
|
+
* opt-in — the consumer deletes its hand-rolled `grid`/`field` styles and its margin cancel in the
|
|
34
|
+
* same edit, and the fleet migrates one app at a time behind visual QA.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Which spacing model a field speaks.
|
|
39
|
+
*
|
|
40
|
+
* A plain string union rather than a `const enum` for the same reason as `FieldLabelVariant`: this
|
|
41
|
+
* type crosses the package boundary into consumers' `.d.ts`, and a cross-boundary `const enum`
|
|
42
|
+
* breaks `isolatedModules` / Babel-only builds.
|
|
43
|
+
*/
|
|
44
|
+
type FieldSpacing = 'stack' | 'gap';
|
|
45
|
+
|
|
4
46
|
/**
|
|
5
47
|
* The TWO legitimate label voices, per the shared forms contract
|
|
6
48
|
* (`PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md` → "The TWO legitimate label voices").
|
|
@@ -102,9 +144,18 @@ interface FieldProps {
|
|
|
102
144
|
* plus a `grow` modifier — instead of having to restate a COMPLETE style per variant.
|
|
103
145
|
*/
|
|
104
146
|
containerStyle?: StyleProp<ViewStyle>;
|
|
147
|
+
/**
|
|
148
|
+
* Which of the two spacing models this field speaks — see `fieldSpacing.ts`.
|
|
149
|
+
*
|
|
150
|
+
* Leave it unset. Inside a `FormGrid` or `FormSection` it resolves to `gap` (the FORMS.md
|
|
151
|
+
* contract: no bottom margin, the parent's `gap` owns the rhythm); anywhere else it resolves to
|
|
152
|
+
* `stack` (the historical 16px margin, byte-identical to 1.8.0). Set it explicitly only to
|
|
153
|
+
* override that — e.g. a cell inside a grid that genuinely stacks two fields wants `stack`.
|
|
154
|
+
*/
|
|
155
|
+
spacing?: FieldSpacing;
|
|
105
156
|
testID?: string;
|
|
106
157
|
}
|
|
107
|
-
declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
158
|
+
declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, spacing, testID, }: FieldProps) => React.ReactElement;
|
|
108
159
|
|
|
109
160
|
/**
|
|
110
161
|
* Required / optional markers, per the shared forms contract
|
|
@@ -137,6 +188,117 @@ type FieldMarker = 'none' | 'required' | 'optional';
|
|
|
137
188
|
*/
|
|
138
189
|
declare const DEFAULT_OPTIONAL_LABEL = "(optional)";
|
|
139
190
|
|
|
191
|
+
/**
|
|
192
|
+
* `FormGrid` — the wrapping form row, extracted from ~177 hand-rolled copies.
|
|
193
|
+
*
|
|
194
|
+
* Verbatim in every portal, differing only in one constant:
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
|
|
198
|
+
* field: { minWidth: 220, flexGrow: 1, flexBasis: 260 }, // or 200/240/260
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* Extracting it is worth more than the deleted lines: because the grid declares itself through
|
|
202
|
+
* `FormGridContext`, a `Field` inside it knows its vertical rhythm is owned by the grid's `gap` and
|
|
203
|
+
* drops its own bottom margin. That kills the fleet's 16 `marginBottom: 0` cancel hacks at the same
|
|
204
|
+
* time — the grid and the cancel were always two halves of one bug.
|
|
205
|
+
*
|
|
206
|
+
* The cell metrics live on the GRID, not on each cell, because that is how the real call sites are
|
|
207
|
+
* written: one `styles.field` reused by every cell in the row, with the occasional narrow one-off.
|
|
208
|
+
* `<FormCell>` with no props is therefore the common case, and `<FormCell basis={160} grow={false}>`
|
|
209
|
+
* the exception — which is exactly the ratio in aml-v2's `ScreeningForm`.
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
interface FormGridProps {
|
|
213
|
+
children: React.ReactNode;
|
|
214
|
+
/**
|
|
215
|
+
* Space between cells, horizontally AND between wrapped rows. This is the value that makes the
|
|
216
|
+
* grid own the vertical rhythm, so it is also what a descendant `Field` is dropping its own
|
|
217
|
+
* margin in favour of.
|
|
218
|
+
*/
|
|
219
|
+
gap?: number;
|
|
220
|
+
/** Default `minWidth` for every `FormCell` in this grid. */
|
|
221
|
+
minWidth?: number;
|
|
222
|
+
/** Default `flexBasis` for every `FormCell` in this grid. */
|
|
223
|
+
basis?: number;
|
|
224
|
+
style?: StyleProp<ViewStyle>;
|
|
225
|
+
testID?: string;
|
|
226
|
+
}
|
|
227
|
+
declare const FormGrid: ({ children, gap, minWidth, basis, style, testID, }: FormGridProps) => React.ReactElement;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `FormCell` — one cell of a `FormGrid`.
|
|
231
|
+
*
|
|
232
|
+
* Replaces the `<View style={styles.field}>` wrapper every real call site already puts around each
|
|
233
|
+
* control. It inherits `minWidth` / `flexBasis` from the enclosing grid so the common case is a bare
|
|
234
|
+
* `<FormCell>`, and takes per-cell overrides for the genuine one-offs (aml-v2's `fieldNarrow`:
|
|
235
|
+
* `minWidth: 140, flexBasis: 160, flexGrow: 0`).
|
|
236
|
+
*
|
|
237
|
+
* Used OUTSIDE a `FormGrid` it still renders a sensible cell from the package defaults rather than
|
|
238
|
+
* throwing — a cell is a layout hint, and a form that renders slightly wide is a better failure than
|
|
239
|
+
* a form that does not render. The grid-owned SPACING behaviour, by contrast, is deliberately NOT
|
|
240
|
+
* available this way: that one has to come from a real grid, because a `Field` dropping its margin
|
|
241
|
+
* with nothing to replace it would collapse the form.
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
interface FormCellProps {
|
|
245
|
+
children: React.ReactNode;
|
|
246
|
+
/** Overrides the grid's `minWidth` for this cell only. */
|
|
247
|
+
minWidth?: number;
|
|
248
|
+
/** Overrides the grid's `basis` for this cell only. */
|
|
249
|
+
basis?: number;
|
|
250
|
+
/** `false` pins the cell to its basis instead of letting it absorb the row's slack. */
|
|
251
|
+
grow?: boolean;
|
|
252
|
+
style?: StyleProp<ViewStyle>;
|
|
253
|
+
testID?: string;
|
|
254
|
+
}
|
|
255
|
+
declare const FormCell: ({ children, minWidth, basis, grow, style, testID, }: FormCellProps) => React.ReactElement;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* `FormSection` — a titled group of fields. The React answer to v1's `<fieldset>` / `<legend>`.
|
|
259
|
+
*
|
|
260
|
+
* v1 has a real `<fieldset>` whose `<legend>` takes the identical label metric as a field label, so
|
|
261
|
+
* a group heading reads as part of the form rather than as page furniture. Nothing in the React
|
|
262
|
+
* fleet has this: zygos fakes it with a generic `Heading`, and everybody else emits an untitled
|
|
263
|
+
* `<View>` — which means the grouping exists visually but not semantically, so a screen-reader user
|
|
264
|
+
* tabbing into "Threshold" is never told it belongs to "Advanced matching".
|
|
265
|
+
*
|
|
266
|
+
* ## Why not a literal `<fieldset>`
|
|
267
|
+
*
|
|
268
|
+
* react-native-web has no `fieldset` mapping — a `View` is a `div`, and there is no escape hatch
|
|
269
|
+
* that turns one into a `<fieldset>` with a real `<legend>`. What IS available is the pattern the
|
|
270
|
+
* ARIA spec defines as the equivalent: `role="group"` + `aria-labelledby` pointing at the heading.
|
|
271
|
+
* Assistive tech announces "Advanced matching, group" exactly as it would for a fieldset. This is
|
|
272
|
+
* the honest ceiling of the platform, not a shortcut — and it is why the legend is a `Text` whose
|
|
273
|
+
* `nativeID` is generated the same way `Field`'s hint/error ids are.
|
|
274
|
+
*
|
|
275
|
+
* The legend deliberately reuses `resolveLabelVariantStyle` — the SAME function `Field` uses for its
|
|
276
|
+
* label — so the two cannot drift. A section titled in one metric above fields labelled in another
|
|
277
|
+
* is precisely the "third voice is drift" failure FORMS.md warns about.
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
interface FormSectionProps {
|
|
281
|
+
/**
|
|
282
|
+
* The legend — pre-localized, like every string in this package. Optional: a section with no
|
|
283
|
+
* title still groups its children for layout, but renders NO legend row and NO `aria-labelledby`,
|
|
284
|
+
* because a group labelled by an empty element is worse for assistive tech than an unlabelled one.
|
|
285
|
+
*/
|
|
286
|
+
title?: string;
|
|
287
|
+
/** Help line for the whole group, under the legend. The group-level sibling of `Field`'s hint. */
|
|
288
|
+
description?: string;
|
|
289
|
+
children: React.ReactNode;
|
|
290
|
+
/**
|
|
291
|
+
* Which label voice the legend speaks. Defaults to `field` — a section heading on a form the user
|
|
292
|
+
* fills in. Pass `control` on a dense surface so the legend matches the controls beneath it.
|
|
293
|
+
*/
|
|
294
|
+
labelVariant?: FieldLabelVariant;
|
|
295
|
+
/** Escape hatch applied AFTER `labelVariant`, for the genuine one-off a variant does not cover. */
|
|
296
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
297
|
+
style?: StyleProp<ViewStyle>;
|
|
298
|
+
testID?: string;
|
|
299
|
+
}
|
|
300
|
+
declare const FormSection: ({ title, description, children, labelVariant, titleStyle, style, testID, }: FormSectionProps) => React.ReactElement;
|
|
301
|
+
|
|
140
302
|
/**
|
|
141
303
|
* `FormActions` — the submit/cancel button row every form ends with.
|
|
142
304
|
*
|
|
@@ -289,7 +451,17 @@ interface ChipSelectorProps<T> {
|
|
|
289
451
|
disabled?: boolean;
|
|
290
452
|
/** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
|
|
291
453
|
variant?: ChipVariant;
|
|
292
|
-
|
|
454
|
+
/**
|
|
455
|
+
* `StyleProp` (not a bare `ViewStyle`) so it composes the same way `Field`'s does — a caller can
|
|
456
|
+
* pass an array instead of restating a complete style per variant.
|
|
457
|
+
*/
|
|
458
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
459
|
+
/**
|
|
460
|
+
* Forwarded verbatim to the underlying `Field`. `ChipSelector` deliberately holds NO spacing
|
|
461
|
+
* opinion of its own: it delegates, so the two cannot drift into disagreeing about how much room
|
|
462
|
+
* a chip block occupies. The guard for that is `chipSelectorFieldLockstep` in the tests.
|
|
463
|
+
*/
|
|
464
|
+
spacing?: FieldSpacing;
|
|
293
465
|
/** Marks the selection mandatory — renders `Field`'s decorative `*` next to the label. */
|
|
294
466
|
required?: boolean;
|
|
295
467
|
/** Validation message under the chips, wired to the chip group via `aria-describedby`. */
|
|
@@ -308,7 +480,7 @@ interface ChipSelectorProps<T> {
|
|
|
308
480
|
*/
|
|
309
481
|
optionAccessibilityHint?: string;
|
|
310
482
|
}
|
|
311
|
-
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
483
|
+
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, spacing, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
312
484
|
|
|
313
485
|
/**
|
|
314
486
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -624,4 +796,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
|
|
|
624
796
|
/** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
|
|
625
797
|
declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
|
|
626
798
|
|
|
627
|
-
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, FormActions, type FormActionsProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useThemedInput };
|
|
799
|
+
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useThemedInput };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { StyleProp, TextStyle, ViewStyle, TextInputProps, TextInput, View } from 'react-native';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* `Field`'s TWO spacing models — the F3 fix, shipped as an OPT-IN rather than a breaking change.
|
|
6
|
+
*
|
|
7
|
+
* ## The defect
|
|
8
|
+
*
|
|
9
|
+
* `Field` hard-coded a 16px bottom margin on its container and 4px margins on its label / hint /
|
|
10
|
+
* error. That is the pre-flexbox model: the CHILD owns the space after itself. In a `gap`-laid-out
|
|
11
|
+
* parent — which is how every modern form row in this fleet is written — the child's margin STACKS
|
|
12
|
+
* on the parent's gap, so the consumer has to cancel it by hand. aml-v2 alone carries 13
|
|
13
|
+
* `marginBottom: 0` cancels, each with an apologetic comment; the fleet carries 16.
|
|
14
|
+
*
|
|
15
|
+
* AML v1 got this right in CSS: `.ui-field { display: flex; flex-direction: column; gap: 6px; }` —
|
|
16
|
+
* internal gap only, and the parent grid owns the space BETWEEN fields. That contract is written up
|
|
17
|
+
* in `PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md` ("The gap is owned by the container,
|
|
18
|
+
* never by margins on the children").
|
|
19
|
+
*
|
|
20
|
+
* ## Why opt-in and not a 2.0.0
|
|
21
|
+
*
|
|
22
|
+
* Switching every `Field` to the gap model at once moves pixels on every form in seven portals in a
|
|
23
|
+
* single publish — which is exactly the change you cannot visually QA, because there is no app you
|
|
24
|
+
* can look at that is still on the old behaviour to compare against. The two models are therefore
|
|
25
|
+
* both implemented, selected per field:
|
|
26
|
+
*
|
|
27
|
+
* - `stack` (DEFAULT) — the historical margins, byte-identical to 1.8.0. Every existing consumer
|
|
28
|
+
* is untouched, including the 16 that currently cancel the margin.
|
|
29
|
+
* - `gap` — the FORMS.md contract: no bottom margin at all, uniform 6px internal gap. The parent
|
|
30
|
+
* owns the rhythm.
|
|
31
|
+
*
|
|
32
|
+
* A `Field` inside a `FormGrid` resolves to `gap` automatically, so adopting the shared grid IS the
|
|
33
|
+
* opt-in — the consumer deletes its hand-rolled `grid`/`field` styles and its margin cancel in the
|
|
34
|
+
* same edit, and the fleet migrates one app at a time behind visual QA.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Which spacing model a field speaks.
|
|
39
|
+
*
|
|
40
|
+
* A plain string union rather than a `const enum` for the same reason as `FieldLabelVariant`: this
|
|
41
|
+
* type crosses the package boundary into consumers' `.d.ts`, and a cross-boundary `const enum`
|
|
42
|
+
* breaks `isolatedModules` / Babel-only builds.
|
|
43
|
+
*/
|
|
44
|
+
type FieldSpacing = 'stack' | 'gap';
|
|
45
|
+
|
|
4
46
|
/**
|
|
5
47
|
* The TWO legitimate label voices, per the shared forms contract
|
|
6
48
|
* (`PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md` → "The TWO legitimate label voices").
|
|
@@ -102,9 +144,18 @@ interface FieldProps {
|
|
|
102
144
|
* plus a `grow` modifier — instead of having to restate a COMPLETE style per variant.
|
|
103
145
|
*/
|
|
104
146
|
containerStyle?: StyleProp<ViewStyle>;
|
|
147
|
+
/**
|
|
148
|
+
* Which of the two spacing models this field speaks — see `fieldSpacing.ts`.
|
|
149
|
+
*
|
|
150
|
+
* Leave it unset. Inside a `FormGrid` or `FormSection` it resolves to `gap` (the FORMS.md
|
|
151
|
+
* contract: no bottom margin, the parent's `gap` owns the rhythm); anywhere else it resolves to
|
|
152
|
+
* `stack` (the historical 16px margin, byte-identical to 1.8.0). Set it explicitly only to
|
|
153
|
+
* override that — e.g. a cell inside a grid that genuinely stacks two fields wants `stack`.
|
|
154
|
+
*/
|
|
155
|
+
spacing?: FieldSpacing;
|
|
105
156
|
testID?: string;
|
|
106
157
|
}
|
|
107
|
-
declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
158
|
+
declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, spacing, testID, }: FieldProps) => React.ReactElement;
|
|
108
159
|
|
|
109
160
|
/**
|
|
110
161
|
* Required / optional markers, per the shared forms contract
|
|
@@ -137,6 +188,117 @@ type FieldMarker = 'none' | 'required' | 'optional';
|
|
|
137
188
|
*/
|
|
138
189
|
declare const DEFAULT_OPTIONAL_LABEL = "(optional)";
|
|
139
190
|
|
|
191
|
+
/**
|
|
192
|
+
* `FormGrid` — the wrapping form row, extracted from ~177 hand-rolled copies.
|
|
193
|
+
*
|
|
194
|
+
* Verbatim in every portal, differing only in one constant:
|
|
195
|
+
*
|
|
196
|
+
* ```ts
|
|
197
|
+
* grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
|
|
198
|
+
* field: { minWidth: 220, flexGrow: 1, flexBasis: 260 }, // or 200/240/260
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* Extracting it is worth more than the deleted lines: because the grid declares itself through
|
|
202
|
+
* `FormGridContext`, a `Field` inside it knows its vertical rhythm is owned by the grid's `gap` and
|
|
203
|
+
* drops its own bottom margin. That kills the fleet's 16 `marginBottom: 0` cancel hacks at the same
|
|
204
|
+
* time — the grid and the cancel were always two halves of one bug.
|
|
205
|
+
*
|
|
206
|
+
* The cell metrics live on the GRID, not on each cell, because that is how the real call sites are
|
|
207
|
+
* written: one `styles.field` reused by every cell in the row, with the occasional narrow one-off.
|
|
208
|
+
* `<FormCell>` with no props is therefore the common case, and `<FormCell basis={160} grow={false}>`
|
|
209
|
+
* the exception — which is exactly the ratio in aml-v2's `ScreeningForm`.
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
interface FormGridProps {
|
|
213
|
+
children: React.ReactNode;
|
|
214
|
+
/**
|
|
215
|
+
* Space between cells, horizontally AND between wrapped rows. This is the value that makes the
|
|
216
|
+
* grid own the vertical rhythm, so it is also what a descendant `Field` is dropping its own
|
|
217
|
+
* margin in favour of.
|
|
218
|
+
*/
|
|
219
|
+
gap?: number;
|
|
220
|
+
/** Default `minWidth` for every `FormCell` in this grid. */
|
|
221
|
+
minWidth?: number;
|
|
222
|
+
/** Default `flexBasis` for every `FormCell` in this grid. */
|
|
223
|
+
basis?: number;
|
|
224
|
+
style?: StyleProp<ViewStyle>;
|
|
225
|
+
testID?: string;
|
|
226
|
+
}
|
|
227
|
+
declare const FormGrid: ({ children, gap, minWidth, basis, style, testID, }: FormGridProps) => React.ReactElement;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* `FormCell` — one cell of a `FormGrid`.
|
|
231
|
+
*
|
|
232
|
+
* Replaces the `<View style={styles.field}>` wrapper every real call site already puts around each
|
|
233
|
+
* control. It inherits `minWidth` / `flexBasis` from the enclosing grid so the common case is a bare
|
|
234
|
+
* `<FormCell>`, and takes per-cell overrides for the genuine one-offs (aml-v2's `fieldNarrow`:
|
|
235
|
+
* `minWidth: 140, flexBasis: 160, flexGrow: 0`).
|
|
236
|
+
*
|
|
237
|
+
* Used OUTSIDE a `FormGrid` it still renders a sensible cell from the package defaults rather than
|
|
238
|
+
* throwing — a cell is a layout hint, and a form that renders slightly wide is a better failure than
|
|
239
|
+
* a form that does not render. The grid-owned SPACING behaviour, by contrast, is deliberately NOT
|
|
240
|
+
* available this way: that one has to come from a real grid, because a `Field` dropping its margin
|
|
241
|
+
* with nothing to replace it would collapse the form.
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
interface FormCellProps {
|
|
245
|
+
children: React.ReactNode;
|
|
246
|
+
/** Overrides the grid's `minWidth` for this cell only. */
|
|
247
|
+
minWidth?: number;
|
|
248
|
+
/** Overrides the grid's `basis` for this cell only. */
|
|
249
|
+
basis?: number;
|
|
250
|
+
/** `false` pins the cell to its basis instead of letting it absorb the row's slack. */
|
|
251
|
+
grow?: boolean;
|
|
252
|
+
style?: StyleProp<ViewStyle>;
|
|
253
|
+
testID?: string;
|
|
254
|
+
}
|
|
255
|
+
declare const FormCell: ({ children, minWidth, basis, grow, style, testID, }: FormCellProps) => React.ReactElement;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* `FormSection` — a titled group of fields. The React answer to v1's `<fieldset>` / `<legend>`.
|
|
259
|
+
*
|
|
260
|
+
* v1 has a real `<fieldset>` whose `<legend>` takes the identical label metric as a field label, so
|
|
261
|
+
* a group heading reads as part of the form rather than as page furniture. Nothing in the React
|
|
262
|
+
* fleet has this: zygos fakes it with a generic `Heading`, and everybody else emits an untitled
|
|
263
|
+
* `<View>` — which means the grouping exists visually but not semantically, so a screen-reader user
|
|
264
|
+
* tabbing into "Threshold" is never told it belongs to "Advanced matching".
|
|
265
|
+
*
|
|
266
|
+
* ## Why not a literal `<fieldset>`
|
|
267
|
+
*
|
|
268
|
+
* react-native-web has no `fieldset` mapping — a `View` is a `div`, and there is no escape hatch
|
|
269
|
+
* that turns one into a `<fieldset>` with a real `<legend>`. What IS available is the pattern the
|
|
270
|
+
* ARIA spec defines as the equivalent: `role="group"` + `aria-labelledby` pointing at the heading.
|
|
271
|
+
* Assistive tech announces "Advanced matching, group" exactly as it would for a fieldset. This is
|
|
272
|
+
* the honest ceiling of the platform, not a shortcut — and it is why the legend is a `Text` whose
|
|
273
|
+
* `nativeID` is generated the same way `Field`'s hint/error ids are.
|
|
274
|
+
*
|
|
275
|
+
* The legend deliberately reuses `resolveLabelVariantStyle` — the SAME function `Field` uses for its
|
|
276
|
+
* label — so the two cannot drift. A section titled in one metric above fields labelled in another
|
|
277
|
+
* is precisely the "third voice is drift" failure FORMS.md warns about.
|
|
278
|
+
*/
|
|
279
|
+
|
|
280
|
+
interface FormSectionProps {
|
|
281
|
+
/**
|
|
282
|
+
* The legend — pre-localized, like every string in this package. Optional: a section with no
|
|
283
|
+
* title still groups its children for layout, but renders NO legend row and NO `aria-labelledby`,
|
|
284
|
+
* because a group labelled by an empty element is worse for assistive tech than an unlabelled one.
|
|
285
|
+
*/
|
|
286
|
+
title?: string;
|
|
287
|
+
/** Help line for the whole group, under the legend. The group-level sibling of `Field`'s hint. */
|
|
288
|
+
description?: string;
|
|
289
|
+
children: React.ReactNode;
|
|
290
|
+
/**
|
|
291
|
+
* Which label voice the legend speaks. Defaults to `field` — a section heading on a form the user
|
|
292
|
+
* fills in. Pass `control` on a dense surface so the legend matches the controls beneath it.
|
|
293
|
+
*/
|
|
294
|
+
labelVariant?: FieldLabelVariant;
|
|
295
|
+
/** Escape hatch applied AFTER `labelVariant`, for the genuine one-off a variant does not cover. */
|
|
296
|
+
titleStyle?: StyleProp<TextStyle>;
|
|
297
|
+
style?: StyleProp<ViewStyle>;
|
|
298
|
+
testID?: string;
|
|
299
|
+
}
|
|
300
|
+
declare const FormSection: ({ title, description, children, labelVariant, titleStyle, style, testID, }: FormSectionProps) => React.ReactElement;
|
|
301
|
+
|
|
140
302
|
/**
|
|
141
303
|
* `FormActions` — the submit/cancel button row every form ends with.
|
|
142
304
|
*
|
|
@@ -289,7 +451,17 @@ interface ChipSelectorProps<T> {
|
|
|
289
451
|
disabled?: boolean;
|
|
290
452
|
/** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
|
|
291
453
|
variant?: ChipVariant;
|
|
292
|
-
|
|
454
|
+
/**
|
|
455
|
+
* `StyleProp` (not a bare `ViewStyle`) so it composes the same way `Field`'s does — a caller can
|
|
456
|
+
* pass an array instead of restating a complete style per variant.
|
|
457
|
+
*/
|
|
458
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
459
|
+
/**
|
|
460
|
+
* Forwarded verbatim to the underlying `Field`. `ChipSelector` deliberately holds NO spacing
|
|
461
|
+
* opinion of its own: it delegates, so the two cannot drift into disagreeing about how much room
|
|
462
|
+
* a chip block occupies. The guard for that is `chipSelectorFieldLockstep` in the tests.
|
|
463
|
+
*/
|
|
464
|
+
spacing?: FieldSpacing;
|
|
293
465
|
/** Marks the selection mandatory — renders `Field`'s decorative `*` next to the label. */
|
|
294
466
|
required?: boolean;
|
|
295
467
|
/** Validation message under the chips, wired to the chip group via `aria-describedby`. */
|
|
@@ -308,7 +480,7 @@ interface ChipSelectorProps<T> {
|
|
|
308
480
|
*/
|
|
309
481
|
optionAccessibilityHint?: string;
|
|
310
482
|
}
|
|
311
|
-
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
483
|
+
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, spacing, required, error, testID, chipTestIDPrefix, optionAccessibilityHint, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
312
484
|
|
|
313
485
|
/**
|
|
314
486
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -624,4 +796,4 @@ declare function suggestOptions(options: readonly ControlOption[], query: string
|
|
|
624
796
|
/** True when `text` is non-empty yet matches no option's value or label exactly (case-insensitive). */
|
|
625
797
|
declare function isUnmatched(options: readonly ControlOption[], text: string): boolean;
|
|
626
798
|
|
|
627
|
-
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, FormActions, type FormActionsProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useThemedInput };
|
|
799
|
+
export { AnchoredMenu, type AnchoredMenuColors, type AnchoredMenuProps, type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, type ControlOption, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, type DateRangeControlProps, type DateRangeValue, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, type FieldSpacing, FormActions, type FormActionsProps, FormCell, type FormCellProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormGrid, type FormGridProps, FormSection, type FormSectionProps, FormSwitch, type FormSwitchProps, SelectControl, type SelectControlProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, TypeaheadControl, type TypeaheadControlProps, type UseThemedInputOptions, controlStyles, isUnmatched, suggestOptions, useThemedInput };
|