@dloizides/ui-forms 1.6.0 → 1.7.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 +59 -0
- package/dist/index.d.mts +163 -9
- package/dist/index.d.ts +163 -9
- package/dist/index.js +217 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +216 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +110 -108
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
Additive wave implementing the shared cross-stack forms contract now written down at
|
|
6
|
+
`PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md`. **Nothing in this release moves a pixel
|
|
7
|
+
for an existing consumer**: every addition is opt-in and every default is the previous behaviour.
|
|
8
|
+
`CONTAINER_MARGIN_BOTTOM` and `ChipSelector`'s compensating `marginBottom: -CHIP_GUTTER` are
|
|
9
|
+
deliberately untouched — they are load-bearing and move together in a later, visual-QA-gated wave.
|
|
10
|
+
|
|
11
|
+
- **Add `FormActions` — the submit/cancel row.** Promoted from the BYTE-IDENTICAL
|
|
12
|
+
`erevna-web/src/components/Forms/FormActions.tsx` and `katalogos-web/.../FormActions.tsx`
|
|
13
|
+
(68 lines each; `diff` returns nothing). All seven portals hand-roll this row; aml-v2, agora,
|
|
14
|
+
kefi, ichnos and zygos each inline their own variant. The primary action is LAST in DOM order so
|
|
15
|
+
tab reaches Cancel before Save, and `saving` drives the primary button's spinner rather than
|
|
16
|
+
disabling-and-relabelling it. `saveDisabled` blocks Save WITHOUT blocking Cancel — an invalid
|
|
17
|
+
form stays escapable. testIDs default to the twins' `save-button` / `cancel-button`, so existing
|
|
18
|
+
Playwright selectors keep matching.
|
|
19
|
+
- **`Field` gains `hint`.** ~21 hand-rolled copies of `{fontSize:12, marginTop:4, color:
|
|
20
|
+
textSecondary}` exist across the fleet; aml-v2 alone declares a local `hint` style in 8 files.
|
|
21
|
+
Because `Field` had no hint slot, consumers rendered the hint as a SIBLING, where it lands past
|
|
22
|
+
the container's 16px bottom margin and detaches from the control it explains (see the apologetic
|
|
23
|
+
comment in `aml-v2/src/screens/screening/CoverageSelector.tsx`). The hint renders UNDER the
|
|
24
|
+
control and ABOVE the error, and hint and error COEXIST — both are reachable from the control via
|
|
25
|
+
a composed `aria-describedby` (hint id then error id, in reading order). Only the error is a
|
|
26
|
+
`role="alert"` live region; a hint that announced itself would interrupt on every focus.
|
|
27
|
+
- **`Field` gains `optional` + `optionalLabel`.** Mutually exclusive with `required`; when a caller
|
|
28
|
+
sets both, `required` WINS — the safe direction, since an under-marked required field sends the
|
|
29
|
+
user into a failed submit. Enforced at runtime in `resolveFieldMarker` rather than in the type
|
|
30
|
+
system, so a caller spreading a computed props object still gets a defined single-marker result
|
|
31
|
+
instead of a compile error it cannot act on. Like the asterisk, the optional text is decorative
|
|
32
|
+
(`aria-hidden`) — "optional" is the ABSENCE of `aria-required`, which assistive tech already
|
|
33
|
+
conveys.
|
|
34
|
+
- **`Field` gains `labelVariant` (+ `labelStyle` as the escape hatch).** `Field` hard-coded
|
|
35
|
+
13/600/sentence-case, which is exactly why kefi-web refused it and why `@dloizides/ui-tables`'
|
|
36
|
+
private `FieldShell` forked to 11/700/uppercase. `labelVariant` makes the contract's TWO
|
|
37
|
+
legitimate voices first-class and named: `field` (13/600 sentence case — a label that is READ)
|
|
38
|
+
and `control` (11/700 UPPERCASE + letter-spacing — a label that is SCANNED, for dense filter bars
|
|
39
|
+
and toolbars). An absent variant resolves to `field`, so every existing consumer renders
|
|
40
|
+
byte-identically. `labelStyle` applies AFTER the variant for the genuine one-off.
|
|
41
|
+
- **`Field`'s `containerStyle` widens `ViewStyle` -> `StyleProp<ViewStyle>`.** The narrow type was a
|
|
42
|
+
documented pain: `aml-v2/src/screens/leaders/CountryPicker.tsx` has to build a COMPLETE style per
|
|
43
|
+
variant because it cannot compose an array.
|
|
44
|
+
- **`useThemedInput`: a focused errored field now keeps a RED focus ring.** It previously computed
|
|
45
|
+
`isFocused && !hasError`, so an errored field got NO focus ring at all — removing the focus
|
|
46
|
+
indicator (WCAG 2.4.7) from precisely the field the user was sent back to fix, at the moment they
|
|
47
|
+
are most likely navigating by keyboard. It now always rings on focus and only changes the ring's
|
|
48
|
+
COLOUR, matching v1's `input.invalid:focus { box-shadow: 0 0 0 3px var(--danger-tint) }`.
|
|
49
|
+
One pre-existing test necessarily changed: it asserted `boxShadow === 'none'` for this state,
|
|
50
|
+
i.e. it encoded the defect as intended behaviour.
|
|
51
|
+
- **New peer dependency `@dloizides/ui-buttons >= 1.4.0`**, required by `FormActions` so the estate
|
|
52
|
+
keeps exactly one button implementation rather than forking one here.
|
|
53
|
+
- No i18n runtime is introduced: all user-visible copy (`saveLabel`, `cancelLabel`, `optionalLabel`,
|
|
54
|
+
`hint`, `error`) arrives pre-localized. aml-v2 localizes via `@dloizides/i18n` with positional
|
|
55
|
+
`{0}` params while the other six apps use i18next `{{p1}}`, and nothing bridges them, so a
|
|
56
|
+
component that localized internally could not serve both.
|
|
57
|
+
- Also shipping in this tarball, from a CONCURRENT workstream (commit `13bd281`, not part of this
|
|
58
|
+
wave): `ChipSelector` gains `chipTestIDPrefix`, `optionAccessibilityHint` and `multiple`
|
|
59
|
+
toggle semantics, and reads its on-primary ink from the theme's `onBrand` scale instead of a
|
|
60
|
+
hardcoded `#fff`. All opt-in; the `chip-selector-chip-<value>` testID default is frozen.
|
|
61
|
+
|
|
3
62
|
## 1.6.0
|
|
4
63
|
|
|
5
64
|
- **`ChipSelector` now composes `Field` internally — its label was drifting.** It rendered its OWN
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { ViewStyle, TextInputProps, TextInput
|
|
2
|
+
import { StyleProp, TextStyle, ViewStyle, TextInputProps, TextInput } from 'react-native';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* The TWO legitimate label voices, per the shared forms contract
|
|
6
|
+
* (`PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md` → "The TWO legitimate label voices").
|
|
7
|
+
*
|
|
8
|
+
* - `field` — 13 / 600 / sentence case. A label that is READ: prose addressed to the person
|
|
9
|
+
* filling the form. This is `Field`'s historical, unchanged default, so every
|
|
10
|
+
* existing consumer renders byte-identically.
|
|
11
|
+
* - `control` — 11 / 700 / UPPERCASE + letter-spacing. A label that is SCANNED: a key over a value
|
|
12
|
+
* in a dense row (filter bars, compact toolbars, pager controls). These are exactly
|
|
13
|
+
* the metrics `@dloizides/ui-tables` forked into its private `FieldShell` because
|
|
14
|
+
* `Field` hard-coded the `field` voice and offered no way out.
|
|
15
|
+
*
|
|
16
|
+
* A THIRD voice is drift, not a variant. Pick the voice from the SURFACE, not the component: every
|
|
17
|
+
* label on one surface uses one voice, and voices are never mixed within a single row.
|
|
18
|
+
*
|
|
19
|
+
* Why a plain string union and not a `const enum`: this type crosses the package boundary into
|
|
20
|
+
* consumers' `.d.ts`, and a cross-boundary `const enum` breaks `isolatedModules` / Babel-only
|
|
21
|
+
* builds. `@dloizides/ui-buttons` sets the same precedent for `ButtonVariant` for the same reason.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Which of the two contract voices a field label speaks. */
|
|
25
|
+
type FieldLabelVariant = 'field' | 'control';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Field — the generic "label over a control, hint + error under it" wrapper.
|
|
6
29
|
*
|
|
7
30
|
* `FormField` is hard-wired to a text input, so any NON-text control (a dropdown, a date picker,
|
|
8
31
|
* a chip selector) had no shared way to get a label — and ended up visually misaligned next to
|
|
@@ -13,13 +36,20 @@ import { ViewStyle, TextInputProps, TextInput, TextStyle } from 'react-native';
|
|
|
13
36
|
* truth: `FormField` composes `Field` internally, so the two can never drift apart.
|
|
14
37
|
*
|
|
15
38
|
* Children may be a plain node (the common case — just wrap your control) or a render function
|
|
16
|
-
* receiving `{ describedById, hasError }`, so a custom control can wire the
|
|
17
|
-
* own `aria-describedby` / invalid state exactly the way `FormField` does.
|
|
39
|
+
* receiving `{ describedById, hasError }`, so a custom control can wire the hint AND error lines
|
|
40
|
+
* into its own `aria-describedby` / invalid state exactly the way `FormField` does.
|
|
41
|
+
*
|
|
42
|
+
* The anatomy (label → control → hint → error), the two label voices, the mutually-exclusive
|
|
43
|
+
* required/optional markers and the collapsing error slot all implement the shared cross-stack
|
|
44
|
+
* contract in `PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md`.
|
|
18
45
|
*/
|
|
19
46
|
|
|
20
47
|
/** What a render-function child receives, so it can mirror `FormField`'s error wiring. */
|
|
21
48
|
interface FieldChildContext {
|
|
22
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Space-separated ids of the lines describing this control — the hint, the error, or both, in
|
|
51
|
+
* reading order. `undefined` when there is neither. Assign straight to `aria-describedby`.
|
|
52
|
+
*/
|
|
23
53
|
describedById?: string;
|
|
24
54
|
hasError: boolean;
|
|
25
55
|
}
|
|
@@ -34,11 +64,123 @@ interface FieldProps {
|
|
|
34
64
|
label?: string;
|
|
35
65
|
children: FieldChildren;
|
|
36
66
|
required?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Marks the field explicitly optional. MUTUALLY EXCLUSIVE with `required` — if both are set,
|
|
69
|
+
* `required` wins (the safe direction: an under-marked required field causes a failed submit).
|
|
70
|
+
* Use it to mark the MINORITY: on a mostly-required form, marking the two optional fields is far
|
|
71
|
+
* less noise than asterisking the other eleven.
|
|
72
|
+
*/
|
|
73
|
+
optional?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Pre-localized text for the optional marker. Defaults to the English `DEFAULT_OPTIONAL_LABEL`
|
|
76
|
+
* so a single-locale app can adopt `optional` in one line; a localized app MUST pass this.
|
|
77
|
+
*/
|
|
78
|
+
optionalLabel?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Help line rendered UNDER the control and ABOVE the error — the sentence that stops the user
|
|
81
|
+
* guessing. It coexists with `error` (the hint states the rule, the error says which rule was
|
|
82
|
+
* broken) and both are reachable from the control via `aria-describedby`.
|
|
83
|
+
*
|
|
84
|
+
* Rendering a hint INSIDE the field is the point: as an outside sibling it lands past the
|
|
85
|
+
* container's 16px bottom margin and detaches from the control it explains.
|
|
86
|
+
*/
|
|
87
|
+
hint?: string;
|
|
37
88
|
error?: string;
|
|
38
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Which of the two contract label voices to speak: `field` (13/600 sentence case — the default,
|
|
91
|
+
* unchanged) or `control` (11/700 uppercase — dense filter bars and toolbars).
|
|
92
|
+
*/
|
|
93
|
+
labelVariant?: FieldLabelVariant;
|
|
94
|
+
/**
|
|
95
|
+
* Escape hatch applied AFTER `labelVariant`, for the genuine one-off a variant does not cover.
|
|
96
|
+
* Prefer `labelVariant`: it is the reviewable, named choice, and a bare `labelStyle` is the thing
|
|
97
|
+
* to grep for when auditing label drift.
|
|
98
|
+
*/
|
|
99
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
100
|
+
/**
|
|
101
|
+
* `StyleProp` (not a bare `ViewStyle`) so a caller can compose an array — e.g. a base field style
|
|
102
|
+
* plus a `grow` modifier — instead of having to restate a COMPLETE style per variant.
|
|
103
|
+
*/
|
|
104
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
39
105
|
testID?: string;
|
|
40
106
|
}
|
|
41
|
-
declare const Field: ({ label, children, required, error, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
107
|
+
declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Required / optional markers, per the shared forms contract
|
|
111
|
+
* (`…/wwwroot/shared/FORMS.md` → "Required and optional markers").
|
|
112
|
+
*
|
|
113
|
+
* The contract says the two markers are MUTUALLY EXCLUSIVE — a field is required or optional, never
|
|
114
|
+
* annotated as both — and that you mark the MINORITY: on a mostly-required form mark the optional
|
|
115
|
+
* fields, on a mostly-optional form mark the required ones. Asterisking twelve of thirteen fields
|
|
116
|
+
* is noise that trains the eye to ignore the marker.
|
|
117
|
+
*
|
|
118
|
+
* Exclusivity is enforced here rather than in the type system so that a caller spreading a props
|
|
119
|
+
* object (`<Field {...fieldProps} />`, where both flags may be computed) still gets a defined,
|
|
120
|
+
* single-marker result instead of a compile error it cannot act on.
|
|
121
|
+
*
|
|
122
|
+
* Precedence when a caller asks for BOTH: `required` wins. That is the safe failure direction —
|
|
123
|
+
* showing "optional" on a field the server will reject sends the user into a failed submit, while
|
|
124
|
+
* showing "required" on a genuinely optional field costs, at worst, an unnecessary keystroke.
|
|
125
|
+
*/
|
|
126
|
+
/** Which marker a field label carries. Exactly one, never both. */
|
|
127
|
+
type FieldMarker = 'none' | 'required' | 'optional';
|
|
128
|
+
/**
|
|
129
|
+
* The English fallback for the optional marker's text.
|
|
130
|
+
*
|
|
131
|
+
* `@dloizides/ui-forms` contains NO i18n runtime by design — aml-v2 uses `@dloizides/i18n` with
|
|
132
|
+
* positional `{0}` params while the other six apps use i18next `{{p1}}`, and nothing bridges them,
|
|
133
|
+
* so all user-visible copy arrives as pre-localized props. This constant exists ONLY so a
|
|
134
|
+
* single-locale app can adopt `optional` in one line. It is exported precisely so it is greppable:
|
|
135
|
+
* **a localized app MUST pass `optionalLabel`.** The required marker needs no such escape — `*` is
|
|
136
|
+
* a glyph, not copy.
|
|
137
|
+
*/
|
|
138
|
+
declare const DEFAULT_OPTIONAL_LABEL = "(optional)";
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `FormActions` — the submit/cancel button row every form ends with.
|
|
142
|
+
*
|
|
143
|
+
* Promoted from the BYTE-IDENTICAL `erevna-web/src/components/Forms/FormActions.tsx` and
|
|
144
|
+
* `katalogos-web/src/components/Forms/FormActions.tsx` (68 lines each, `diff` returns nothing);
|
|
145
|
+
* all seven apps hand-roll the same row. Buttons come from the shared `@dloizides/ui-buttons` kit
|
|
146
|
+
* rather than being forked here, so there is exactly one button implementation in the estate.
|
|
147
|
+
*
|
|
148
|
+
* The layout implements the contract's actions row
|
|
149
|
+
* (`…/wwwroot/shared/FORMS.md` → "Actions row"): the primary action is LAST in DOM order, so tab
|
|
150
|
+
* order reaches Cancel before Save, and `saving` drives the primary button's spinner rather than
|
|
151
|
+
* disabling-and-relabelling it.
|
|
152
|
+
*
|
|
153
|
+
* Two minimal generalizations over the app-local twins:
|
|
154
|
+
* 1. Labels and hints arrive as PRE-LOCALIZED props. The twins called `FM()` internally, which
|
|
155
|
+
* cannot work here: aml-v2 localizes via `@dloizides/i18n` with positional `{0}` params while
|
|
156
|
+
* the other six use i18next `{{p1}}`, and nothing bridges them.
|
|
157
|
+
* 2. `testID`s are overridable but default to the twins' `save-button` / `cancel-button`, so the
|
|
158
|
+
* existing Playwright selectors in every app keep matching.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
interface FormActionsProps {
|
|
162
|
+
/** Submit handler. May be async — the shared Button drives its own spinner for the promise. */
|
|
163
|
+
onSave: () => void | Promise<unknown>;
|
|
164
|
+
/** Cancel handler. Omit it entirely to render a save-only row (no empty Cancel slot). */
|
|
165
|
+
onCancel?: () => void | Promise<unknown>;
|
|
166
|
+
/** Pre-localized label for the primary action. */
|
|
167
|
+
saveLabel: string;
|
|
168
|
+
/** Pre-localized label for the cancel action. Required whenever `onCancel` is supplied. */
|
|
169
|
+
cancelLabel?: string;
|
|
170
|
+
/** Pre-localized accessibility hint for the primary action. Falls back to its label. */
|
|
171
|
+
saveHint?: string;
|
|
172
|
+
/** Pre-localized accessibility hint for the cancel action. Falls back to its label. */
|
|
173
|
+
cancelHint?: string;
|
|
174
|
+
/** In-flight submit: spins the primary action and disables cancel so the form can't be abandoned mid-write. */
|
|
175
|
+
saving?: boolean;
|
|
176
|
+
/** Disables the primary action independently of `saving` (e.g. an invalid form). */
|
|
177
|
+
saveDisabled?: boolean;
|
|
178
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
179
|
+
saveTestID?: string;
|
|
180
|
+
cancelTestID?: string;
|
|
181
|
+
testID?: string;
|
|
182
|
+
}
|
|
183
|
+
declare const FormActions: ({ onSave, onCancel, saveLabel, cancelLabel, saveHint, cancelHint, saving, saveDisabled, containerStyle, saveTestID, cancelTestID, testID, }: FormActionsProps) => React.ReactElement;
|
|
42
184
|
|
|
43
185
|
/**
|
|
44
186
|
* Reusable form field — labelled text input with optional required mark + error.
|
|
@@ -153,8 +295,20 @@ interface ChipSelectorProps<T> {
|
|
|
153
295
|
/** Validation message under the chips, wired to the chip group via `aria-describedby`. */
|
|
154
296
|
error?: string;
|
|
155
297
|
testID?: string;
|
|
298
|
+
/**
|
|
299
|
+
* Stem for each chip's testID — a chip becomes `${chipTestIDPrefix}-${value}`. Defaults to
|
|
300
|
+
* `chip-selector-chip`, which existing specs select on; supply your own when a screen has
|
|
301
|
+
* several chip groups that must be addressed independently.
|
|
302
|
+
*/
|
|
303
|
+
chipTestIDPrefix?: string;
|
|
304
|
+
/**
|
|
305
|
+
* Accessibility hint applied to every chip in the group. Supply a LOCALIZED string — the
|
|
306
|
+
* built-in default (`Selects <label>`) is English-only and exists solely so pre-existing
|
|
307
|
+
* consumers keep their current announcement.
|
|
308
|
+
*/
|
|
309
|
+
optionAccessibilityHint?: string;
|
|
156
310
|
}
|
|
157
|
-
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, required, error, testID, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
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;
|
|
158
312
|
|
|
159
313
|
/**
|
|
160
314
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -197,4 +351,4 @@ interface FormCheckboxProps {
|
|
|
197
351
|
}
|
|
198
352
|
declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
|
|
199
353
|
|
|
200
|
-
export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, Field, type FieldChildContext, type FieldChildren, type FieldProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
|
|
354
|
+
export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, DEFAULT_OPTIONAL_LABEL, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, FormActions, type FormActionsProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { ViewStyle, TextInputProps, TextInput
|
|
2
|
+
import { StyleProp, TextStyle, ViewStyle, TextInputProps, TextInput } from 'react-native';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* The TWO legitimate label voices, per the shared forms contract
|
|
6
|
+
* (`PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md` → "The TWO legitimate label voices").
|
|
7
|
+
*
|
|
8
|
+
* - `field` — 13 / 600 / sentence case. A label that is READ: prose addressed to the person
|
|
9
|
+
* filling the form. This is `Field`'s historical, unchanged default, so every
|
|
10
|
+
* existing consumer renders byte-identically.
|
|
11
|
+
* - `control` — 11 / 700 / UPPERCASE + letter-spacing. A label that is SCANNED: a key over a value
|
|
12
|
+
* in a dense row (filter bars, compact toolbars, pager controls). These are exactly
|
|
13
|
+
* the metrics `@dloizides/ui-tables` forked into its private `FieldShell` because
|
|
14
|
+
* `Field` hard-coded the `field` voice and offered no way out.
|
|
15
|
+
*
|
|
16
|
+
* A THIRD voice is drift, not a variant. Pick the voice from the SURFACE, not the component: every
|
|
17
|
+
* label on one surface uses one voice, and voices are never mixed within a single row.
|
|
18
|
+
*
|
|
19
|
+
* Why a plain string union and not a `const enum`: this type crosses the package boundary into
|
|
20
|
+
* consumers' `.d.ts`, and a cross-boundary `const enum` breaks `isolatedModules` / Babel-only
|
|
21
|
+
* builds. `@dloizides/ui-buttons` sets the same precedent for `ButtonVariant` for the same reason.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Which of the two contract voices a field label speaks. */
|
|
25
|
+
type FieldLabelVariant = 'field' | 'control';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Field — the generic "label over a control, hint + error under it" wrapper.
|
|
6
29
|
*
|
|
7
30
|
* `FormField` is hard-wired to a text input, so any NON-text control (a dropdown, a date picker,
|
|
8
31
|
* a chip selector) had no shared way to get a label — and ended up visually misaligned next to
|
|
@@ -13,13 +36,20 @@ import { ViewStyle, TextInputProps, TextInput, TextStyle } from 'react-native';
|
|
|
13
36
|
* truth: `FormField` composes `Field` internally, so the two can never drift apart.
|
|
14
37
|
*
|
|
15
38
|
* Children may be a plain node (the common case — just wrap your control) or a render function
|
|
16
|
-
* receiving `{ describedById, hasError }`, so a custom control can wire the
|
|
17
|
-
* own `aria-describedby` / invalid state exactly the way `FormField` does.
|
|
39
|
+
* receiving `{ describedById, hasError }`, so a custom control can wire the hint AND error lines
|
|
40
|
+
* into its own `aria-describedby` / invalid state exactly the way `FormField` does.
|
|
41
|
+
*
|
|
42
|
+
* The anatomy (label → control → hint → error), the two label voices, the mutually-exclusive
|
|
43
|
+
* required/optional markers and the collapsing error slot all implement the shared cross-stack
|
|
44
|
+
* contract in `PROOViD/AMLService/AMLService/wwwroot/shared/FORMS.md`.
|
|
18
45
|
*/
|
|
19
46
|
|
|
20
47
|
/** What a render-function child receives, so it can mirror `FormField`'s error wiring. */
|
|
21
48
|
interface FieldChildContext {
|
|
22
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Space-separated ids of the lines describing this control — the hint, the error, or both, in
|
|
51
|
+
* reading order. `undefined` when there is neither. Assign straight to `aria-describedby`.
|
|
52
|
+
*/
|
|
23
53
|
describedById?: string;
|
|
24
54
|
hasError: boolean;
|
|
25
55
|
}
|
|
@@ -34,11 +64,123 @@ interface FieldProps {
|
|
|
34
64
|
label?: string;
|
|
35
65
|
children: FieldChildren;
|
|
36
66
|
required?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Marks the field explicitly optional. MUTUALLY EXCLUSIVE with `required` — if both are set,
|
|
69
|
+
* `required` wins (the safe direction: an under-marked required field causes a failed submit).
|
|
70
|
+
* Use it to mark the MINORITY: on a mostly-required form, marking the two optional fields is far
|
|
71
|
+
* less noise than asterisking the other eleven.
|
|
72
|
+
*/
|
|
73
|
+
optional?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Pre-localized text for the optional marker. Defaults to the English `DEFAULT_OPTIONAL_LABEL`
|
|
76
|
+
* so a single-locale app can adopt `optional` in one line; a localized app MUST pass this.
|
|
77
|
+
*/
|
|
78
|
+
optionalLabel?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Help line rendered UNDER the control and ABOVE the error — the sentence that stops the user
|
|
81
|
+
* guessing. It coexists with `error` (the hint states the rule, the error says which rule was
|
|
82
|
+
* broken) and both are reachable from the control via `aria-describedby`.
|
|
83
|
+
*
|
|
84
|
+
* Rendering a hint INSIDE the field is the point: as an outside sibling it lands past the
|
|
85
|
+
* container's 16px bottom margin and detaches from the control it explains.
|
|
86
|
+
*/
|
|
87
|
+
hint?: string;
|
|
37
88
|
error?: string;
|
|
38
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Which of the two contract label voices to speak: `field` (13/600 sentence case — the default,
|
|
91
|
+
* unchanged) or `control` (11/700 uppercase — dense filter bars and toolbars).
|
|
92
|
+
*/
|
|
93
|
+
labelVariant?: FieldLabelVariant;
|
|
94
|
+
/**
|
|
95
|
+
* Escape hatch applied AFTER `labelVariant`, for the genuine one-off a variant does not cover.
|
|
96
|
+
* Prefer `labelVariant`: it is the reviewable, named choice, and a bare `labelStyle` is the thing
|
|
97
|
+
* to grep for when auditing label drift.
|
|
98
|
+
*/
|
|
99
|
+
labelStyle?: StyleProp<TextStyle>;
|
|
100
|
+
/**
|
|
101
|
+
* `StyleProp` (not a bare `ViewStyle`) so a caller can compose an array — e.g. a base field style
|
|
102
|
+
* plus a `grow` modifier — instead of having to restate a COMPLETE style per variant.
|
|
103
|
+
*/
|
|
104
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
39
105
|
testID?: string;
|
|
40
106
|
}
|
|
41
|
-
declare const Field: ({ label, children, required, error, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
107
|
+
declare const Field: ({ label, children, required, optional, optionalLabel, hint, error, labelVariant, labelStyle, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Required / optional markers, per the shared forms contract
|
|
111
|
+
* (`…/wwwroot/shared/FORMS.md` → "Required and optional markers").
|
|
112
|
+
*
|
|
113
|
+
* The contract says the two markers are MUTUALLY EXCLUSIVE — a field is required or optional, never
|
|
114
|
+
* annotated as both — and that you mark the MINORITY: on a mostly-required form mark the optional
|
|
115
|
+
* fields, on a mostly-optional form mark the required ones. Asterisking twelve of thirteen fields
|
|
116
|
+
* is noise that trains the eye to ignore the marker.
|
|
117
|
+
*
|
|
118
|
+
* Exclusivity is enforced here rather than in the type system so that a caller spreading a props
|
|
119
|
+
* object (`<Field {...fieldProps} />`, where both flags may be computed) still gets a defined,
|
|
120
|
+
* single-marker result instead of a compile error it cannot act on.
|
|
121
|
+
*
|
|
122
|
+
* Precedence when a caller asks for BOTH: `required` wins. That is the safe failure direction —
|
|
123
|
+
* showing "optional" on a field the server will reject sends the user into a failed submit, while
|
|
124
|
+
* showing "required" on a genuinely optional field costs, at worst, an unnecessary keystroke.
|
|
125
|
+
*/
|
|
126
|
+
/** Which marker a field label carries. Exactly one, never both. */
|
|
127
|
+
type FieldMarker = 'none' | 'required' | 'optional';
|
|
128
|
+
/**
|
|
129
|
+
* The English fallback for the optional marker's text.
|
|
130
|
+
*
|
|
131
|
+
* `@dloizides/ui-forms` contains NO i18n runtime by design — aml-v2 uses `@dloizides/i18n` with
|
|
132
|
+
* positional `{0}` params while the other six apps use i18next `{{p1}}`, and nothing bridges them,
|
|
133
|
+
* so all user-visible copy arrives as pre-localized props. This constant exists ONLY so a
|
|
134
|
+
* single-locale app can adopt `optional` in one line. It is exported precisely so it is greppable:
|
|
135
|
+
* **a localized app MUST pass `optionalLabel`.** The required marker needs no such escape — `*` is
|
|
136
|
+
* a glyph, not copy.
|
|
137
|
+
*/
|
|
138
|
+
declare const DEFAULT_OPTIONAL_LABEL = "(optional)";
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `FormActions` — the submit/cancel button row every form ends with.
|
|
142
|
+
*
|
|
143
|
+
* Promoted from the BYTE-IDENTICAL `erevna-web/src/components/Forms/FormActions.tsx` and
|
|
144
|
+
* `katalogos-web/src/components/Forms/FormActions.tsx` (68 lines each, `diff` returns nothing);
|
|
145
|
+
* all seven apps hand-roll the same row. Buttons come from the shared `@dloizides/ui-buttons` kit
|
|
146
|
+
* rather than being forked here, so there is exactly one button implementation in the estate.
|
|
147
|
+
*
|
|
148
|
+
* The layout implements the contract's actions row
|
|
149
|
+
* (`…/wwwroot/shared/FORMS.md` → "Actions row"): the primary action is LAST in DOM order, so tab
|
|
150
|
+
* order reaches Cancel before Save, and `saving` drives the primary button's spinner rather than
|
|
151
|
+
* disabling-and-relabelling it.
|
|
152
|
+
*
|
|
153
|
+
* Two minimal generalizations over the app-local twins:
|
|
154
|
+
* 1. Labels and hints arrive as PRE-LOCALIZED props. The twins called `FM()` internally, which
|
|
155
|
+
* cannot work here: aml-v2 localizes via `@dloizides/i18n` with positional `{0}` params while
|
|
156
|
+
* the other six use i18next `{{p1}}`, and nothing bridges them.
|
|
157
|
+
* 2. `testID`s are overridable but default to the twins' `save-button` / `cancel-button`, so the
|
|
158
|
+
* existing Playwright selectors in every app keep matching.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
interface FormActionsProps {
|
|
162
|
+
/** Submit handler. May be async — the shared Button drives its own spinner for the promise. */
|
|
163
|
+
onSave: () => void | Promise<unknown>;
|
|
164
|
+
/** Cancel handler. Omit it entirely to render a save-only row (no empty Cancel slot). */
|
|
165
|
+
onCancel?: () => void | Promise<unknown>;
|
|
166
|
+
/** Pre-localized label for the primary action. */
|
|
167
|
+
saveLabel: string;
|
|
168
|
+
/** Pre-localized label for the cancel action. Required whenever `onCancel` is supplied. */
|
|
169
|
+
cancelLabel?: string;
|
|
170
|
+
/** Pre-localized accessibility hint for the primary action. Falls back to its label. */
|
|
171
|
+
saveHint?: string;
|
|
172
|
+
/** Pre-localized accessibility hint for the cancel action. Falls back to its label. */
|
|
173
|
+
cancelHint?: string;
|
|
174
|
+
/** In-flight submit: spins the primary action and disables cancel so the form can't be abandoned mid-write. */
|
|
175
|
+
saving?: boolean;
|
|
176
|
+
/** Disables the primary action independently of `saving` (e.g. an invalid form). */
|
|
177
|
+
saveDisabled?: boolean;
|
|
178
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
179
|
+
saveTestID?: string;
|
|
180
|
+
cancelTestID?: string;
|
|
181
|
+
testID?: string;
|
|
182
|
+
}
|
|
183
|
+
declare const FormActions: ({ onSave, onCancel, saveLabel, cancelLabel, saveHint, cancelHint, saving, saveDisabled, containerStyle, saveTestID, cancelTestID, testID, }: FormActionsProps) => React.ReactElement;
|
|
42
184
|
|
|
43
185
|
/**
|
|
44
186
|
* Reusable form field — labelled text input with optional required mark + error.
|
|
@@ -153,8 +295,20 @@ interface ChipSelectorProps<T> {
|
|
|
153
295
|
/** Validation message under the chips, wired to the chip group via `aria-describedby`. */
|
|
154
296
|
error?: string;
|
|
155
297
|
testID?: string;
|
|
298
|
+
/**
|
|
299
|
+
* Stem for each chip's testID — a chip becomes `${chipTestIDPrefix}-${value}`. Defaults to
|
|
300
|
+
* `chip-selector-chip`, which existing specs select on; supply your own when a screen has
|
|
301
|
+
* several chip groups that must be addressed independently.
|
|
302
|
+
*/
|
|
303
|
+
chipTestIDPrefix?: string;
|
|
304
|
+
/**
|
|
305
|
+
* Accessibility hint applied to every chip in the group. Supply a LOCALIZED string — the
|
|
306
|
+
* built-in default (`Selects <label>`) is English-only and exists solely so pre-existing
|
|
307
|
+
* consumers keep their current announcement.
|
|
308
|
+
*/
|
|
309
|
+
optionAccessibilityHint?: string;
|
|
156
310
|
}
|
|
157
|
-
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, required, error, testID, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
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;
|
|
158
312
|
|
|
159
313
|
/**
|
|
160
314
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -197,4 +351,4 @@ interface FormCheckboxProps {
|
|
|
197
351
|
}
|
|
198
352
|
declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
|
|
199
353
|
|
|
200
|
-
export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, Field, type FieldChildContext, type FieldChildren, type FieldProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
|
|
354
|
+
export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, DEFAULT_OPTIONAL_LABEL, Field, type FieldChildContext, type FieldChildren, type FieldLabelVariant, type FieldMarker, type FieldProps, FormActions, type FormActionsProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
|