@dloizides/ui-forms 1.5.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 +29 -0
- package/README.md +107 -75
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +50 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +40 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
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
|
+
|
|
3
32
|
## 1.5.0
|
|
4
33
|
|
|
5
34
|
- **Add `Field` — the generic label-over-control wrapper.** `FormField` is hard-wired to a text
|
package/README.md
CHANGED
|
@@ -1,75 +1,107 @@
|
|
|
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`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
@@ -25,7 +25,13 @@ interface FieldChildContext {
|
|
|
25
25
|
}
|
|
26
26
|
type FieldChildren = React.ReactNode | ((context: FieldChildContext) => React.ReactNode);
|
|
27
27
|
interface FieldProps {
|
|
28
|
-
|
|
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;
|
|
29
35
|
children: FieldChildren;
|
|
30
36
|
required?: boolean;
|
|
31
37
|
error?: string;
|
|
@@ -117,6 +123,12 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
|
|
|
117
123
|
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
118
124
|
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
119
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.
|
|
120
132
|
*/
|
|
121
133
|
|
|
122
134
|
/** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
|
|
@@ -126,6 +138,7 @@ interface ChipOption<T> {
|
|
|
126
138
|
label: string;
|
|
127
139
|
}
|
|
128
140
|
interface ChipSelectorProps<T> {
|
|
141
|
+
/** Optional — with no label, no label row is rendered (no phantom gap above the chips). */
|
|
129
142
|
label?: string;
|
|
130
143
|
options: Array<ChipOption<T>>;
|
|
131
144
|
value: T | T[];
|
|
@@ -135,8 +148,13 @@ interface ChipSelectorProps<T> {
|
|
|
135
148
|
/** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
|
|
136
149
|
variant?: ChipVariant;
|
|
137
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;
|
|
138
156
|
}
|
|
139
|
-
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;
|
|
140
158
|
|
|
141
159
|
/**
|
|
142
160
|
* Reusable form switch/toggle with an optional description.
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,13 @@ interface FieldChildContext {
|
|
|
25
25
|
}
|
|
26
26
|
type FieldChildren = React.ReactNode | ((context: FieldChildContext) => React.ReactNode);
|
|
27
27
|
interface FieldProps {
|
|
28
|
-
|
|
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;
|
|
29
35
|
children: FieldChildren;
|
|
30
36
|
required?: boolean;
|
|
31
37
|
error?: string;
|
|
@@ -117,6 +123,12 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
|
|
|
117
123
|
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
118
124
|
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
119
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.
|
|
120
132
|
*/
|
|
121
133
|
|
|
122
134
|
/** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
|
|
@@ -126,6 +138,7 @@ interface ChipOption<T> {
|
|
|
126
138
|
label: string;
|
|
127
139
|
}
|
|
128
140
|
interface ChipSelectorProps<T> {
|
|
141
|
+
/** Optional — with no label, no label row is rendered (no phantom gap above the chips). */
|
|
129
142
|
label?: string;
|
|
130
143
|
options: Array<ChipOption<T>>;
|
|
131
144
|
value: T | T[];
|
|
@@ -135,8 +148,13 @@ interface ChipSelectorProps<T> {
|
|
|
135
148
|
/** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
|
|
136
149
|
variant?: ChipVariant;
|
|
137
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;
|
|
138
156
|
}
|
|
139
|
-
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;
|
|
140
158
|
|
|
141
159
|
/**
|
|
142
160
|
* Reusable form switch/toggle with an optional description.
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React = require('react');
|
|
4
4
|
var reactNative = require('react-native');
|
|
5
5
|
var uiFeedback = require('@dloizides/ui-feedback');
|
|
6
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
7
|
|
|
8
8
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
9
|
|
|
10
|
-
var
|
|
10
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
11
11
|
|
|
12
12
|
// src/Field/Field.tsx
|
|
13
13
|
var CONTAINER_MARGIN_BOTTOM = 16;
|
|
@@ -32,7 +32,7 @@ var styles = reactNative.StyleSheet.create({
|
|
|
32
32
|
});
|
|
33
33
|
var fieldSeq = 0;
|
|
34
34
|
function useFieldErrorId() {
|
|
35
|
-
const [id] =
|
|
35
|
+
const [id] = React__default.default.useState(() => {
|
|
36
36
|
fieldSeq += 1;
|
|
37
37
|
return `field-error-${String(fieldSeq)}`;
|
|
38
38
|
});
|
|
@@ -53,8 +53,9 @@ var Field = ({
|
|
|
53
53
|
const { colors, semantic } = theme;
|
|
54
54
|
const errorColor = semantic.error["500"];
|
|
55
55
|
const hasError = hasFieldError(error);
|
|
56
|
+
const hasLabel = typeof label === "string" && label !== "";
|
|
56
57
|
const errorId = useFieldErrorId();
|
|
57
|
-
const themeStyles =
|
|
58
|
+
const themeStyles = React__default.default.useMemo(
|
|
58
59
|
() => ({
|
|
59
60
|
label: { color: colors.textSecondary },
|
|
60
61
|
requiredMark: { color: errorColor },
|
|
@@ -64,7 +65,7 @@ var Field = ({
|
|
|
64
65
|
);
|
|
65
66
|
const control = typeof children === "function" ? children({ describedById: hasError ? errorId : void 0, hasError }) : children;
|
|
66
67
|
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.container, containerStyle], testID, children: [
|
|
67
|
-
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles.label, themeStyles.label], children: [
|
|
68
|
+
hasLabel ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles.label, themeStyles.label], children: [
|
|
68
69
|
label,
|
|
69
70
|
" ",
|
|
70
71
|
required ? (
|
|
@@ -72,7 +73,7 @@ var Field = ({
|
|
|
72
73
|
// assistive tech, so a screen reader says "required" instead of "star".
|
|
73
74
|
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: themeStyles.requiredMark, children: "*" })
|
|
74
75
|
) : null
|
|
75
|
-
] }),
|
|
76
|
+
] }) : null,
|
|
76
77
|
control,
|
|
77
78
|
hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
|
|
78
79
|
] });
|
|
@@ -109,27 +110,27 @@ function useThemedInput(options = {}) {
|
|
|
109
110
|
const { hasError = false, onFocus, onBlur } = options;
|
|
110
111
|
const { theme } = uiFeedback.useUi();
|
|
111
112
|
const { colors, palette, semantic } = theme;
|
|
112
|
-
const [isFocused, setIsFocused] =
|
|
113
|
-
const [isHovered, setIsHovered] =
|
|
114
|
-
const handleFocus =
|
|
113
|
+
const [isFocused, setIsFocused] = React.useState(false);
|
|
114
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
115
|
+
const handleFocus = React.useCallback(
|
|
115
116
|
(e) => {
|
|
116
117
|
setIsFocused(true);
|
|
117
118
|
onFocus?.(e);
|
|
118
119
|
},
|
|
119
120
|
[onFocus]
|
|
120
121
|
);
|
|
121
|
-
const handleBlur =
|
|
122
|
+
const handleBlur = React.useCallback(
|
|
122
123
|
(e) => {
|
|
123
124
|
setIsFocused(false);
|
|
124
125
|
onBlur?.(e);
|
|
125
126
|
},
|
|
126
127
|
[onBlur]
|
|
127
128
|
);
|
|
128
|
-
const handleHoverIn =
|
|
129
|
-
const handleHoverOut =
|
|
129
|
+
const handleHoverIn = React.useCallback(() => setIsHovered(true), []);
|
|
130
|
+
const handleHoverOut = React.useCallback(() => setIsHovered(false), []);
|
|
130
131
|
const primary = palette.primary["500"];
|
|
131
132
|
const errorColor = semantic.error["500"];
|
|
132
|
-
const style =
|
|
133
|
+
const style = React.useMemo(() => {
|
|
133
134
|
const restBorder = hasError ? errorColor : colors.border;
|
|
134
135
|
const focusBorder = hasError ? errorColor : primary;
|
|
135
136
|
const borderColor = isFocused ? focusBorder : isHovered ? withAlpha(colors.text, HOVER_BORDER_ALPHA) : restBorder;
|
|
@@ -157,8 +158,8 @@ function useThemedInput(options = {}) {
|
|
|
157
158
|
primary,
|
|
158
159
|
errorColor
|
|
159
160
|
]);
|
|
160
|
-
const focusBind =
|
|
161
|
-
const hoverBind =
|
|
161
|
+
const focusBind = React.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
|
|
162
|
+
const hoverBind = React.useMemo(
|
|
162
163
|
() => IS_WEB ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
|
|
163
164
|
[handleHoverIn, handleHoverOut]
|
|
164
165
|
);
|
|
@@ -172,7 +173,7 @@ function useThemedInput(options = {}) {
|
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
var IS_WEB2 = reactNative.Platform.OS === "web";
|
|
175
|
-
var ThemedTextInput =
|
|
176
|
+
var ThemedTextInput = React__default.default.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
|
|
176
177
|
const {
|
|
177
178
|
style: themedStyle,
|
|
178
179
|
placeholderTextColor: themedPlaceholder,
|
|
@@ -244,6 +245,7 @@ var RGB_R_END = 2;
|
|
|
244
245
|
var RGB_G_END = 4;
|
|
245
246
|
var RGB_B_END = 6;
|
|
246
247
|
var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
|
|
248
|
+
var CHIP_GUTTER = 8;
|
|
247
249
|
function withAlpha2(hex, alpha) {
|
|
248
250
|
const normalized = hex.replace("#", "");
|
|
249
251
|
const isShort = normalized.length === HEX_SHORT_LENGTH2;
|
|
@@ -255,17 +257,17 @@ function withAlpha2(hex, alpha) {
|
|
|
255
257
|
return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
|
|
256
258
|
}
|
|
257
259
|
var styles3 = reactNative.StyleSheet.create({
|
|
258
|
-
label: {
|
|
259
|
-
marginBottom: 8,
|
|
260
|
-
fontWeight: "600"
|
|
261
|
-
},
|
|
262
260
|
chipContainer: {
|
|
263
261
|
flexDirection: "row",
|
|
264
|
-
flexWrap: "wrap"
|
|
262
|
+
flexWrap: "wrap",
|
|
263
|
+
// Each chip's bottom gutter also hangs off the LAST row, stacking on `Field`'s container
|
|
264
|
+
// margin (8 + 16 = 24) and dropping the block below the `FormField` beside it. Cancelling it
|
|
265
|
+
// is standard gutter compensation: inner rows keep their 8px, the block ends at Field's 16.
|
|
266
|
+
marginBottom: -CHIP_GUTTER
|
|
265
267
|
},
|
|
266
268
|
chipWrapper: {
|
|
267
|
-
marginRight:
|
|
268
|
-
marginBottom:
|
|
269
|
+
marginRight: CHIP_GUTTER,
|
|
270
|
+
marginBottom: CHIP_GUTTER
|
|
269
271
|
},
|
|
270
272
|
chip: {
|
|
271
273
|
paddingHorizontal: 12,
|
|
@@ -290,7 +292,7 @@ function OutlineChip({
|
|
|
290
292
|
colors,
|
|
291
293
|
onPress
|
|
292
294
|
}) {
|
|
293
|
-
const [hovered, setHovered] =
|
|
295
|
+
const [hovered, setHovered] = React__default.default.useState(false);
|
|
294
296
|
const active = selected || hovered;
|
|
295
297
|
const borderColor = active ? colors.primary : colors.border;
|
|
296
298
|
const backgroundColor = selected ? colors.tint : colors.muted;
|
|
@@ -338,6 +340,11 @@ function SolidChip({
|
|
|
338
340
|
}
|
|
339
341
|
);
|
|
340
342
|
}
|
|
343
|
+
var IS_WEB3 = reactNative.Platform.OS === "web";
|
|
344
|
+
function groupA11yProps(describedById, hasError) {
|
|
345
|
+
if (!IS_WEB3) return {};
|
|
346
|
+
return { "aria-invalid": hasError ? true : void 0, "aria-describedby": describedById };
|
|
347
|
+
}
|
|
341
348
|
var ChipSelector = ({
|
|
342
349
|
label,
|
|
343
350
|
options,
|
|
@@ -346,12 +353,14 @@ var ChipSelector = ({
|
|
|
346
353
|
multiple = false,
|
|
347
354
|
disabled = false,
|
|
348
355
|
variant = "solid",
|
|
349
|
-
containerStyle
|
|
356
|
+
containerStyle,
|
|
357
|
+
required = false,
|
|
358
|
+
error,
|
|
359
|
+
testID
|
|
350
360
|
}) => {
|
|
351
361
|
const { theme } = uiFeedback.useUi();
|
|
352
362
|
const { colors, palette } = theme;
|
|
353
|
-
const
|
|
354
|
-
const chipColors = React3__default.default.useMemo(() => {
|
|
363
|
+
const chipColors = React__default.default.useMemo(() => {
|
|
355
364
|
const primary = palette.primary["500"];
|
|
356
365
|
return {
|
|
357
366
|
border: colors.border,
|
|
@@ -362,40 +371,23 @@ var ChipSelector = ({
|
|
|
362
371
|
tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
|
|
363
372
|
};
|
|
364
373
|
}, [colors.border, colors.text, colors.textSecondary, colors.background, palette.primary]);
|
|
365
|
-
const labelStyle = React3__default.default.useMemo(() => ({ color: colors.text }), [colors.text]);
|
|
366
374
|
function isSelected(optionValue) {
|
|
367
375
|
if (multiple && Array.isArray(value))
|
|
368
376
|
return value.includes(optionValue);
|
|
369
377
|
return value === optionValue;
|
|
370
378
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
onPress
|
|
384
|
-
},
|
|
385
|
-
String(option.value)
|
|
386
|
-
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
387
|
-
SolidChip,
|
|
388
|
-
{
|
|
389
|
-
colors: chipColors,
|
|
390
|
-
disabled,
|
|
391
|
-
option,
|
|
392
|
-
selected,
|
|
393
|
-
onPress
|
|
394
|
-
},
|
|
395
|
-
String(option.value)
|
|
396
|
-
);
|
|
397
|
-
}) })
|
|
398
|
-
] });
|
|
379
|
+
function renderChip(option) {
|
|
380
|
+
const chipProps = {
|
|
381
|
+
colors: chipColors,
|
|
382
|
+
disabled,
|
|
383
|
+
option,
|
|
384
|
+
selected: isSelected(option.value),
|
|
385
|
+
onPress: () => onChange(option.value)
|
|
386
|
+
};
|
|
387
|
+
const key = String(option.value);
|
|
388
|
+
return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(OutlineChip, { ...chipProps }, key) : /* @__PURE__ */ jsxRuntime.jsx(SolidChip, { ...chipProps }, key);
|
|
389
|
+
}
|
|
390
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Field, { containerStyle, error, label, required, testID, children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.chipContainer, ...groupA11yProps(describedById, hasError), children: options.map(renderChip) }) });
|
|
399
391
|
};
|
|
400
392
|
var DESCRIPTION_FONT_SIZE = 12;
|
|
401
393
|
var styles4 = reactNative.StyleSheet.create({
|
|
@@ -429,7 +421,7 @@ var FormSwitch = ({
|
|
|
429
421
|
const { theme } = uiFeedback.useUi();
|
|
430
422
|
const { colors } = theme;
|
|
431
423
|
const hasDescription = typeof description === "string" && description !== "";
|
|
432
|
-
const themeStyles =
|
|
424
|
+
const themeStyles = React__default.default.useMemo(() => {
|
|
433
425
|
const labelStyle = { color: colors.text };
|
|
434
426
|
const descriptionStyle = { color: colors.textSecondary };
|
|
435
427
|
return { label: labelStyle, description: descriptionStyle };
|
|
@@ -520,7 +512,7 @@ var FormCheckbox = ({
|
|
|
520
512
|
const brand = palette.primary["500"];
|
|
521
513
|
const hintText = typeof hint === "string" && hint !== "" ? hint : description;
|
|
522
514
|
const hasHint = typeof hintText === "string" && hintText !== "";
|
|
523
|
-
const themeStyles =
|
|
515
|
+
const themeStyles = React__default.default.useMemo(() => {
|
|
524
516
|
const boxOn = { backgroundColor: brand, borderColor: brand };
|
|
525
517
|
const boxOff = { backgroundColor: "transparent", borderColor: colors.border };
|
|
526
518
|
const labelStyle = { color: colors.text };
|