@dloizides/ui-forms 1.4.0 → 1.6.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 CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0
4
+
5
+ - **`ChipSelector` now composes `Field` internally — its label was drifting.** It rendered its OWN
6
+ `<Text>` label whose metrics differed from `Field`'s on three of four axes: `marginBottom: 8`
7
+ (vs 4), NO `fontSize` at all so it fell back to RN's ~14 (vs 13), and `colors.text`
8
+ (vs `textSecondary`). A `ChipSelector` next to a `FormField` in a row/grid therefore started a
9
+ different distance down the column and read as a different weight of text. Found at 4 sites in
10
+ aml-v2 (`TenantWatchlistsEditor`, `CoverageSelector`, `CustomCoverageFields`,
11
+ `AdvancedMatching`); fixing it in the package fixes every consumer with zero app churn.
12
+ The local label styles are deleted — there is now exactly one label implementation in the kit.
13
+ - **`ChipSelectorProps` is unchanged and fully backward-compatible** — every existing prop keeps
14
+ its name, type and default, and `label` stays OPTIONAL. With no label (or an empty one) NO label
15
+ row is rendered, so composing `Field` cannot introduce a phantom gap above the chips.
16
+ - **`ChipSelector` gains `required`, `error` and `testID`** (all optional, all additive) now that
17
+ `Field` provides them for free. `error` renders the same `role="alert"` line as `FormField` and
18
+ is tied to the chip group via `aria-describedby` + `aria-invalid` (web only; no-op on native).
19
+ - **Spacing:** a `ChipSelector` now carries `Field`'s `marginBottom: 16` like every other field
20
+ (it previously had none). The chips' own 8px trailing gutter is cancelled on the group so the
21
+ block's total bottom spacing is exactly 16 — the same as a `FormField`, not 24. Pass
22
+ `containerStyle` to override, as before.
23
+ - **`Field`'s `label` is now optional** (widened from `label: string`, so existing callers are
24
+ unaffected). An absent or empty label renders no label row, letting a control whose label lives
25
+ elsewhere still use the shared shell for its error line and spacing.
26
+ - `FormSwitch` / `FormCheckbox` are deliberately NOT changed — see `src/labelRoles.test.tsx`.
27
+ Their labels are INLINE control labels (beside the control in a row, the row's primary text),
28
+ not field headers stacked above a control, so they have no alignment defect to fix and `Field`'s
29
+ smaller/dimmer header treatment would be a regression. They were already identical to each
30
+ other; a new test now enforces that they stay so.
31
+
32
+ ## 1.5.0
33
+
34
+ - **Add `Field` — the generic label-over-control wrapper.** `FormField` is hard-wired to a text
35
+ input (`FormFieldProps extends Omit<TextInputProps,'style'>`), so any NON-text control (a
36
+ dropdown, a date picker, a chip selector) had no shared way to get a label — and ended up
37
+ visually misaligned next to its `FormField` siblings: the control's box started ~21px higher
38
+ (no label row) and it missed `FormField`'s `marginBottom: 16`, so wrapped rows lost their
39
+ vertical rhythm. This was hand-rolled in 5 places across zygos-web and aml-v2.
40
+ `Field` takes `{ label, required?, error?, children, containerStyle?, testID? }` and renders the
41
+ same label row / required mark / error line / spacing.
42
+ - **`FormField` now composes `Field` internally**, so the two can never drift apart — the metrics
43
+ live in exactly one place. `FormFieldProps` and every rendered attribute (`form-field-input`
44
+ testID, `aria-invalid` / `aria-describedby` / `aria-required`, the `role="alert"` error line)
45
+ are unchanged: fully backward-compatible.
46
+ - `Field`'s `children` may be a plain node (the common case) or a render function receiving
47
+ `{ describedById, hasError }`, so a custom control can wire the error line into its own
48
+ `aria-describedby` / invalid state exactly the way `FormField` does.
49
+
3
50
  ## 1.4.0
4
51
 
5
52
  - **Accessibility (WCAG 2.1 AA) hardening — additive + backward-compatible.**
package/README.md CHANGED
@@ -1,37 +1,107 @@
1
- # @dloizides/ui-forms
2
-
3
- Themable, brand-agnostic React Native (RN-web) **form** components for the dloizides.com
4
- portfolio: `FormField`, `ChipSelector`, `FormSwitch`. They read theme colours from the shared
5
- `@dloizides/ui-feedback` UI context (`useUi`) — mount one `UiProvider` / `FeedbackUiProvider`
6
- at your app root and these components pick up your theme automatically.
7
-
8
- ## Install
9
-
10
- ```bash
11
- npm install @dloizides/ui-forms @dloizides/ui-feedback
12
- ```
13
-
14
- Peer dependencies: `@dloizides/ui-feedback >= 1.1.0`, `react >= 18`, `react-native >= 0.74`
15
- (use `react-native-web` on web).
16
-
17
- ## Usage
18
-
19
- ```tsx
20
- import { FormField, ChipSelector, FormSwitch } from '@dloizides/ui-forms';
21
-
22
- <FormField label="Email" required error={errors.email} value={email} onChangeText={setEmail} />
23
- <ChipSelector label="Plan" options={plans} value={plan} onChange={setPlan} />
24
- <FormSwitch label="Email notifications" value={notify} onValueChange={setNotify} />
25
- ```
26
-
27
- The injected theme needs `colors.{surface,text,textSecondary,border}`, `palette.primary['500']`
28
- and `semantic.error['500']` — supplied via `@dloizides/ui-feedback`'s provider. Without a provider
29
- the components fall back to a neutral default theme.
30
-
31
- ## testIDs
32
-
33
- `form-field-input`, `chip-selector-chip-<value>`, `form-switch` (overridable on FormSwitch).
34
-
35
- ## License
36
-
37
- MIT
1
+ # @dloizides/ui-forms
2
+
3
+ Themable, brand-agnostic React Native (RN-web) **form** components for the dloizides.com
4
+ portfolio: `Field`, `FormField`, `ChipSelector`, `FormSwitch`. They read theme colours from the shared
5
+ `@dloizides/ui-feedback` UI context (`useUi`) — mount one `UiProvider` / `FeedbackUiProvider`
6
+ at your app root and these components pick up your theme automatically.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @dloizides/ui-forms @dloizides/ui-feedback
12
+ ```
13
+
14
+ Peer dependencies: `@dloizides/ui-feedback >= 1.1.0`, `react >= 18`, `react-native >= 0.74`
15
+ (use `react-native-web` on web).
16
+
17
+ ## Usage
18
+
19
+ ```tsx
20
+ import { FormField, ChipSelector, FormSwitch } from '@dloizides/ui-forms';
21
+
22
+ <FormField label="Email" required error={errors.email} value={email} onChangeText={setEmail} />
23
+ <ChipSelector label="Plan" options={plans} value={plan} onChange={setPlan} />
24
+ <FormSwitch label="Email notifications" value={notify} onValueChange={setNotify} />
25
+ ```
26
+
27
+ ## `Field` — label any control, not just a text input
28
+
29
+ `FormField` is hard-wired to a text input. For **any other control** — a dropdown, a date picker,
30
+ a chip selector — wrap it in `Field` to get the identical label row, required mark, error line and
31
+ bottom spacing. This is what keeps a dropdown aligned with the `FormField` next to it: without it
32
+ the dropdown's box starts a label-row higher and the row loses its vertical rhythm.
33
+
34
+ ```tsx
35
+ import { Field } from '@dloizides/ui-forms';
36
+
37
+ <Field label="Currency" required error={errors.currency}>
38
+ <ModalDropdown options={currencies} value={currency} onChange={setCurrency} />
39
+ </Field>
40
+ ```
41
+
42
+ `FormField` **and** `ChipSelector` compose `Field` internally, so the label row can never drift
43
+ out of alignment between them — there is exactly one label implementation in the kit.
44
+
45
+ To wire the error line into a custom control's own `aria-describedby` / invalid state, pass a
46
+ **function** child — it receives `{ describedById, hasError }` (`describedById` is `undefined`
47
+ when there is no error):
48
+
49
+ ```tsx
50
+ <Field label="Country" error={errors.country}>
51
+ {({ describedById, hasError }) => (
52
+ <CountryPicker aria-describedby={describedById} hasError={hasError} value={country} onChange={setCountry} />
53
+ )}
54
+ </Field>
55
+ ```
56
+
57
+ | Prop | Type | Notes |
58
+ |------|------|-------|
59
+ | `label` | `string` | optional — an absent or empty label renders NO label row |
60
+ | `children` | `ReactNode \| (ctx) => ReactNode` | the control |
61
+ | `required` | `boolean` | renders a decorative `*` (hidden from assistive tech) |
62
+ | `error` | `string` | renders a `role="alert"` line, linked via the child's `describedById` |
63
+ | `containerStyle` | `ViewStyle` | merged over the container |
64
+ | `testID` | `string` | on the container |
65
+
66
+ The injected theme needs `colors.{surface,text,textSecondary,border}`, `palette.primary['500']`
67
+ and `semantic.error['500']` — supplied via `@dloizides/ui-feedback`'s provider. Without a provider
68
+ the components fall back to a neutral default theme.
69
+
70
+ ## `ChipSelector`
71
+
72
+ ```tsx
73
+ <ChipSelector
74
+ label="Plan"
75
+ required
76
+ error={errors.plan}
77
+ options={plans}
78
+ value={plan}
79
+ onChange={setPlan}
80
+ variant="outline"
81
+ />
82
+ ```
83
+
84
+ Its label, required mark, error line and bottom spacing come from `Field`. `label` is optional:
85
+ with no label no label row is rendered, so it drops into a toolbar or filter bar cleanly.
86
+
87
+ | Prop | Type | Notes |
88
+ |------|------|-------|
89
+ | `label` | `string` | optional — no label means no label row |
90
+ | `options` | `Array<{ value, label }>` | `value` is a `string` or `number` |
91
+ | `value` | `T \| T[]` | array when `multiple` |
92
+ | `onChange` | `(value: T) => void` | fires the pressed value; the parent owns toggle logic |
93
+ | `multiple` | `boolean` | selection is an array |
94
+ | `disabled` | `boolean` | disables every chip |
95
+ | `variant` | `'solid' \| 'outline'` | `solid` (default) = filled pill; `outline` = tinted outline |
96
+ | `required` | `boolean` | renders `Field`'s decorative `*` |
97
+ | `error` | `string` | `role="alert"` line, tied to the chip group via `aria-describedby` |
98
+ | `containerStyle` | `ViewStyle` | merged over the container |
99
+ | `testID` | `string` | on the container |
100
+
101
+ ## testIDs
102
+
103
+ `form-field-input`, `chip-selector-chip-<value>`, `form-switch` (overridable on FormSwitch).
104
+
105
+ ## License
106
+
107
+ MIT
package/dist/index.d.mts CHANGED
@@ -1,12 +1,55 @@
1
1
  import React from 'react';
2
- import { TextInputProps, ViewStyle, TextInput, TextStyle } from 'react-native';
2
+ import { ViewStyle, TextInputProps, TextInput, TextStyle } from 'react-native';
3
+
4
+ /**
5
+ * Field — the generic "label over a control, error under it" wrapper.
6
+ *
7
+ * `FormField` is hard-wired to a text input, so any NON-text control (a dropdown, a date picker,
8
+ * a chip selector) had no shared way to get a label — and ended up visually misaligned next to
9
+ * its `FormField` siblings: its box started a label-row higher, and it missed the container's
10
+ * bottom margin so wrapped rows lost their vertical rhythm.
11
+ *
12
+ * `Field` is that label row + spacing, control-agnostic. Its metrics are the single source of
13
+ * truth: `FormField` composes `Field` internally, so the two can never drift apart.
14
+ *
15
+ * 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 error line into its
17
+ * own `aria-describedby` / invalid state exactly the way `FormField` does.
18
+ */
19
+
20
+ /** What a render-function child receives, so it can mirror `FormField`'s error wiring. */
21
+ interface FieldChildContext {
22
+ /** id of the rendered error line, or `undefined` when there is no error. */
23
+ describedById?: string;
24
+ hasError: boolean;
25
+ }
26
+ type FieldChildren = React.ReactNode | ((context: FieldChildContext) => React.ReactNode);
27
+ interface FieldProps {
28
+ /**
29
+ * Label shown above the control. Optional so a control whose label is supplied elsewhere (or
30
+ * which genuinely has none) can still use the shared shell for its error line + spacing: an
31
+ * absent or empty label renders NO label row at all, rather than an empty one that would add a
32
+ * phantom gap and misalign the control against its labelled siblings.
33
+ */
34
+ label?: string;
35
+ children: FieldChildren;
36
+ required?: boolean;
37
+ error?: string;
38
+ containerStyle?: ViewStyle;
39
+ testID?: string;
40
+ }
41
+ declare const Field: ({ label, children, required, error, containerStyle, testID, }: FieldProps) => React.ReactElement;
3
42
 
4
43
  /**
5
44
  * Reusable form field — labelled text input with optional required mark + error.
6
45
  *
7
- * The input itself is the shared `ThemedTextInput` primitive, so every FormField gets the
8
- * readable + lively treatment (dark text, subtle off-white rest background, brand focus
9
- * animation with a soft ring) for free, driven by the active theme.
46
+ * The label row, the required mark, the error line and the container spacing all come from the
47
+ * generic `Field` wrapper, so a `FormField` and a `Field`-wrapped dropdown sitting next to each
48
+ * other line up exactly and can never drift apart. FormField only adds the control itself.
49
+ *
50
+ * The input is the shared `ThemedTextInput` primitive, so every FormField gets the readable +
51
+ * lively treatment (dark text, subtle off-white rest background, brand focus animation with a
52
+ * soft ring) for free, driven by the active theme.
10
53
  */
11
54
 
12
55
  interface FormFieldProps extends Omit<TextInputProps, 'style'> {
@@ -80,6 +123,12 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
80
123
  * - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
81
124
  * tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
82
125
  * Hover (web) lifts the border/ink to the primary colour.
126
+ *
127
+ * The label row, the error line and the container spacing come from the shared `Field` wrapper —
128
+ * NOT from a local `<Text>`. Its own label used to drift from `Field` on three of four axes
129
+ * (`marginBottom: 8` vs 4, no `fontSize` at all so ~14 vs 13, `colors.text` vs `textSecondary`),
130
+ * so a `ChipSelector` next to a `FormField` in a row/grid started a different distance down the
131
+ * column and read as a different weight of text. Now there is exactly one label implementation.
83
132
  */
84
133
 
85
134
  /** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
@@ -89,6 +138,7 @@ interface ChipOption<T> {
89
138
  label: string;
90
139
  }
91
140
  interface ChipSelectorProps<T> {
141
+ /** Optional — with no label, no label row is rendered (no phantom gap above the chips). */
92
142
  label?: string;
93
143
  options: Array<ChipOption<T>>;
94
144
  value: T | T[];
@@ -98,8 +148,13 @@ interface ChipSelectorProps<T> {
98
148
  /** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
99
149
  variant?: ChipVariant;
100
150
  containerStyle?: ViewStyle;
151
+ /** Marks the selection mandatory — renders `Field`'s decorative `*` next to the label. */
152
+ required?: boolean;
153
+ /** Validation message under the chips, wired to the chip group via `aria-describedby`. */
154
+ error?: string;
155
+ testID?: string;
101
156
  }
102
- declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
157
+ declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, required, error, testID, }: ChipSelectorProps<T>) => React.ReactElement;
103
158
 
104
159
  /**
105
160
  * Reusable form switch/toggle with an optional description.
@@ -142,4 +197,4 @@ interface FormCheckboxProps {
142
197
  }
143
198
  declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
144
199
 
145
- export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
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 };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,55 @@
1
1
  import React from 'react';
2
- import { TextInputProps, ViewStyle, TextInput, TextStyle } from 'react-native';
2
+ import { ViewStyle, TextInputProps, TextInput, TextStyle } from 'react-native';
3
+
4
+ /**
5
+ * Field — the generic "label over a control, error under it" wrapper.
6
+ *
7
+ * `FormField` is hard-wired to a text input, so any NON-text control (a dropdown, a date picker,
8
+ * a chip selector) had no shared way to get a label — and ended up visually misaligned next to
9
+ * its `FormField` siblings: its box started a label-row higher, and it missed the container's
10
+ * bottom margin so wrapped rows lost their vertical rhythm.
11
+ *
12
+ * `Field` is that label row + spacing, control-agnostic. Its metrics are the single source of
13
+ * truth: `FormField` composes `Field` internally, so the two can never drift apart.
14
+ *
15
+ * 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 error line into its
17
+ * own `aria-describedby` / invalid state exactly the way `FormField` does.
18
+ */
19
+
20
+ /** What a render-function child receives, so it can mirror `FormField`'s error wiring. */
21
+ interface FieldChildContext {
22
+ /** id of the rendered error line, or `undefined` when there is no error. */
23
+ describedById?: string;
24
+ hasError: boolean;
25
+ }
26
+ type FieldChildren = React.ReactNode | ((context: FieldChildContext) => React.ReactNode);
27
+ interface FieldProps {
28
+ /**
29
+ * Label shown above the control. Optional so a control whose label is supplied elsewhere (or
30
+ * which genuinely has none) can still use the shared shell for its error line + spacing: an
31
+ * absent or empty label renders NO label row at all, rather than an empty one that would add a
32
+ * phantom gap and misalign the control against its labelled siblings.
33
+ */
34
+ label?: string;
35
+ children: FieldChildren;
36
+ required?: boolean;
37
+ error?: string;
38
+ containerStyle?: ViewStyle;
39
+ testID?: string;
40
+ }
41
+ declare const Field: ({ label, children, required, error, containerStyle, testID, }: FieldProps) => React.ReactElement;
3
42
 
4
43
  /**
5
44
  * Reusable form field — labelled text input with optional required mark + error.
6
45
  *
7
- * The input itself is the shared `ThemedTextInput` primitive, so every FormField gets the
8
- * readable + lively treatment (dark text, subtle off-white rest background, brand focus
9
- * animation with a soft ring) for free, driven by the active theme.
46
+ * The label row, the required mark, the error line and the container spacing all come from the
47
+ * generic `Field` wrapper, so a `FormField` and a `Field`-wrapped dropdown sitting next to each
48
+ * other line up exactly and can never drift apart. FormField only adds the control itself.
49
+ *
50
+ * The input is the shared `ThemedTextInput` primitive, so every FormField gets the readable +
51
+ * lively treatment (dark text, subtle off-white rest background, brand focus animation with a
52
+ * soft ring) for free, driven by the active theme.
10
53
  */
11
54
 
12
55
  interface FormFieldProps extends Omit<TextInputProps, 'style'> {
@@ -80,6 +123,12 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
80
123
  * - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
81
124
  * tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
82
125
  * Hover (web) lifts the border/ink to the primary colour.
126
+ *
127
+ * The label row, the error line and the container spacing come from the shared `Field` wrapper —
128
+ * NOT from a local `<Text>`. Its own label used to drift from `Field` on three of four axes
129
+ * (`marginBottom: 8` vs 4, no `fontSize` at all so ~14 vs 13, `colors.text` vs `textSecondary`),
130
+ * so a `ChipSelector` next to a `FormField` in a row/grid started a different distance down the
131
+ * column and read as a different weight of text. Now there is exactly one label implementation.
83
132
  */
84
133
 
85
134
  /** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
@@ -89,6 +138,7 @@ interface ChipOption<T> {
89
138
  label: string;
90
139
  }
91
140
  interface ChipSelectorProps<T> {
141
+ /** Optional — with no label, no label row is rendered (no phantom gap above the chips). */
92
142
  label?: string;
93
143
  options: Array<ChipOption<T>>;
94
144
  value: T | T[];
@@ -98,8 +148,13 @@ interface ChipSelectorProps<T> {
98
148
  /** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
99
149
  variant?: ChipVariant;
100
150
  containerStyle?: ViewStyle;
151
+ /** Marks the selection mandatory — renders `Field`'s decorative `*` next to the label. */
152
+ required?: boolean;
153
+ /** Validation message under the chips, wired to the chip group via `aria-describedby`. */
154
+ error?: string;
155
+ testID?: string;
101
156
  }
102
- declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
157
+ declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, required, error, testID, }: ChipSelectorProps<T>) => React.ReactElement;
103
158
 
104
159
  /**
105
160
  * Reusable form switch/toggle with an optional description.
@@ -142,4 +197,4 @@ interface FormCheckboxProps {
142
197
  }
143
198
  declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
144
199
 
145
- export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
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 };