@dloizides/ui-forms 1.3.0 → 1.5.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 +36 -0
- package/README.md +39 -1
- package/dist/index.d.mts +49 -5
- package/dist/index.d.ts +49 -5
- package/dist/index.js +132 -73
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +133 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
- **Add `Field` — the generic label-over-control wrapper.** `FormField` is hard-wired to a text
|
|
6
|
+
input (`FormFieldProps extends Omit<TextInputProps,'style'>`), so any NON-text control (a
|
|
7
|
+
dropdown, a date picker, a chip selector) had no shared way to get a label — and ended up
|
|
8
|
+
visually misaligned next to its `FormField` siblings: the control's box started ~21px higher
|
|
9
|
+
(no label row) and it missed `FormField`'s `marginBottom: 16`, so wrapped rows lost their
|
|
10
|
+
vertical rhythm. This was hand-rolled in 5 places across zygos-web and aml-v2.
|
|
11
|
+
`Field` takes `{ label, required?, error?, children, containerStyle?, testID? }` and renders the
|
|
12
|
+
same label row / required mark / error line / spacing.
|
|
13
|
+
- **`FormField` now composes `Field` internally**, so the two can never drift apart — the metrics
|
|
14
|
+
live in exactly one place. `FormFieldProps` and every rendered attribute (`form-field-input`
|
|
15
|
+
testID, `aria-invalid` / `aria-describedby` / `aria-required`, the `role="alert"` error line)
|
|
16
|
+
are unchanged: fully backward-compatible.
|
|
17
|
+
- `Field`'s `children` may be a plain node (the common case) or a render function receiving
|
|
18
|
+
`{ describedById, hasError }`, so a custom control can wire the error line into its own
|
|
19
|
+
`aria-describedby` / invalid state exactly the way `FormField` does.
|
|
20
|
+
|
|
21
|
+
## 1.4.0
|
|
22
|
+
|
|
23
|
+
- **Accessibility (WCAG 2.1 AA) hardening — additive + backward-compatible.**
|
|
24
|
+
- `FormField`: the error message is now programmatically tied to the input — the input gets
|
|
25
|
+
`aria-invalid` when errored and `aria-describedby` pointing at the error line (which is a
|
|
26
|
+
`role="alert"` live region), and `aria-required` when `required`. The required `*` is marked
|
|
27
|
+
decorative (`aria-hidden` / `accessibilityElementsHidden`) so a screen reader hears "required"
|
|
28
|
+
(via `aria-required`) instead of "star".
|
|
29
|
+
- `ThemedTextInput`: new optional `describedById` + `requiredField` props forward
|
|
30
|
+
`aria-describedby` / `aria-required`, and it emits `aria-invalid` from `hasError` (web only;
|
|
31
|
+
no-ops on native). Existing callers are unaffected.
|
|
32
|
+
- `FormCheckbox`: now keyboard-operable — the `role="checkbox"` Pressable renders a `<div>`
|
|
33
|
+
(no free Enter/Space), so an explicit web-only Enter/Space handler toggles it (WCAG 2.1.1).
|
|
34
|
+
- `ChipSelector`: chips gain a `hitSlop` so the touch target reaches ~44px WITHOUT changing the
|
|
35
|
+
rendered pill size, and expose `accessibilityState.disabled` when the selector is disabled.
|
|
36
|
+
- `useThemedInput`: the focus/hover CSS transition now collapses to instant when the user
|
|
37
|
+
requests `prefers-reduced-motion: reduce` (WCAG 2.3.3, web only).
|
|
38
|
+
|
|
3
39
|
## 1.3.0
|
|
4
40
|
|
|
5
41
|
- `ChipSelector` gains a `variant` prop (`'solid' | 'outline'`, default `'solid'`). `solid` is the
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @dloizides/ui-forms
|
|
2
2
|
|
|
3
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
|
|
4
|
+
portfolio: `Field`, `FormField`, `ChipSelector`, `FormSwitch`. They read theme colours from the shared
|
|
5
5
|
`@dloizides/ui-feedback` UI context (`useUi`) — mount one `UiProvider` / `FeedbackUiProvider`
|
|
6
6
|
at your app root and these components pick up your theme automatically.
|
|
7
7
|
|
|
@@ -24,6 +24,44 @@ import { FormField, ChipSelector, FormSwitch } from '@dloizides/ui-forms';
|
|
|
24
24
|
<FormSwitch label="Email notifications" value={notify} onValueChange={setNotify} />
|
|
25
25
|
```
|
|
26
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` composes `Field` internally, so the two can never drift out of alignment.
|
|
43
|
+
|
|
44
|
+
To wire the error line into a custom control's own `aria-describedby` / invalid state, pass a
|
|
45
|
+
**function** child — it receives `{ describedById, hasError }` (`describedById` is `undefined`
|
|
46
|
+
when there is no error):
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<Field label="Country" error={errors.country}>
|
|
50
|
+
{({ describedById, hasError }) => (
|
|
51
|
+
<CountryPicker aria-describedby={describedById} hasError={hasError} value={country} onChange={setCountry} />
|
|
52
|
+
)}
|
|
53
|
+
</Field>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Prop | Type | Notes |
|
|
57
|
+
|------|------|-------|
|
|
58
|
+
| `label` | `string` | required |
|
|
59
|
+
| `children` | `ReactNode \| (ctx) => ReactNode` | the control |
|
|
60
|
+
| `required` | `boolean` | renders a decorative `*` (hidden from assistive tech) |
|
|
61
|
+
| `error` | `string` | renders a `role="alert"` line, linked via the child's `describedById` |
|
|
62
|
+
| `containerStyle` | `ViewStyle` | merged over the container |
|
|
63
|
+
| `testID` | `string` | on the container |
|
|
64
|
+
|
|
27
65
|
The injected theme needs `colors.{surface,text,textSecondary,border}`, `palette.primary['500']`
|
|
28
66
|
and `semantic.error['500']` — supplied via `@dloizides/ui-feedback`'s provider. Without a provider
|
|
29
67
|
the components fall back to a neutral default theme.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
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
|
+
label: string;
|
|
29
|
+
children: FieldChildren;
|
|
30
|
+
required?: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
containerStyle?: ViewStyle;
|
|
33
|
+
testID?: string;
|
|
34
|
+
}
|
|
35
|
+
declare const Field: ({ label, children, required, error, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
3
36
|
|
|
4
37
|
/**
|
|
5
38
|
* Reusable form field — labelled text input with optional required mark + error.
|
|
6
39
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
40
|
+
* The label row, the required mark, the error line and the container spacing all come from the
|
|
41
|
+
* generic `Field` wrapper, so a `FormField` and a `Field`-wrapped dropdown sitting next to each
|
|
42
|
+
* other line up exactly and can never drift apart. FormField only adds the control itself.
|
|
43
|
+
*
|
|
44
|
+
* The input is the shared `ThemedTextInput` primitive, so every FormField gets the readable +
|
|
45
|
+
* lively treatment (dark text, subtle off-white rest background, brand focus animation with a
|
|
46
|
+
* soft ring) for free, driven by the active theme.
|
|
10
47
|
*/
|
|
11
48
|
|
|
12
49
|
interface FormFieldProps extends Omit<TextInputProps, 'style'> {
|
|
@@ -33,6 +70,13 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
|
|
|
33
70
|
interface ThemedTextInputProps extends TextInputProps {
|
|
34
71
|
/** Render with the error border colour (matches `FormField`'s error treatment). */
|
|
35
72
|
hasError?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* id of an element describing this input (e.g. an error/hint line). Linked via
|
|
75
|
+
* `aria-describedby` on web so a screen reader reads it after the field's name. No-op native.
|
|
76
|
+
*/
|
|
77
|
+
describedById?: string;
|
|
78
|
+
/** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
|
|
79
|
+
requiredField?: boolean;
|
|
36
80
|
}
|
|
37
81
|
declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
|
|
38
82
|
|
|
@@ -135,4 +179,4 @@ interface FormCheckboxProps {
|
|
|
135
179
|
}
|
|
136
180
|
declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
|
|
137
181
|
|
|
138
|
-
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 };
|
|
182
|
+
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,49 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
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
|
+
label: string;
|
|
29
|
+
children: FieldChildren;
|
|
30
|
+
required?: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
containerStyle?: ViewStyle;
|
|
33
|
+
testID?: string;
|
|
34
|
+
}
|
|
35
|
+
declare const Field: ({ label, children, required, error, containerStyle, testID, }: FieldProps) => React.ReactElement;
|
|
3
36
|
|
|
4
37
|
/**
|
|
5
38
|
* Reusable form field — labelled text input with optional required mark + error.
|
|
6
39
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
40
|
+
* The label row, the required mark, the error line and the container spacing all come from the
|
|
41
|
+
* generic `Field` wrapper, so a `FormField` and a `Field`-wrapped dropdown sitting next to each
|
|
42
|
+
* other line up exactly and can never drift apart. FormField only adds the control itself.
|
|
43
|
+
*
|
|
44
|
+
* The input is the shared `ThemedTextInput` primitive, so every FormField gets the readable +
|
|
45
|
+
* lively treatment (dark text, subtle off-white rest background, brand focus animation with a
|
|
46
|
+
* soft ring) for free, driven by the active theme.
|
|
10
47
|
*/
|
|
11
48
|
|
|
12
49
|
interface FormFieldProps extends Omit<TextInputProps, 'style'> {
|
|
@@ -33,6 +70,13 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
|
|
|
33
70
|
interface ThemedTextInputProps extends TextInputProps {
|
|
34
71
|
/** Render with the error border colour (matches `FormField`'s error treatment). */
|
|
35
72
|
hasError?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* id of an element describing this input (e.g. an error/hint line). Linked via
|
|
75
|
+
* `aria-describedby` on web so a screen reader reads it after the field's name. No-op native.
|
|
76
|
+
*/
|
|
77
|
+
describedById?: string;
|
|
78
|
+
/** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
|
|
79
|
+
requiredField?: boolean;
|
|
36
80
|
}
|
|
37
81
|
declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
|
|
38
82
|
|
|
@@ -135,4 +179,4 @@ interface FormCheckboxProps {
|
|
|
135
179
|
}
|
|
136
180
|
declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
|
|
137
181
|
|
|
138
|
-
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 };
|
|
182
|
+
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.js
CHANGED
|
@@ -9,7 +9,74 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
9
9
|
|
|
10
10
|
var React3__default = /*#__PURE__*/_interopDefault(React3);
|
|
11
11
|
|
|
12
|
-
// src/
|
|
12
|
+
// src/Field/Field.tsx
|
|
13
|
+
var CONTAINER_MARGIN_BOTTOM = 16;
|
|
14
|
+
var LABEL_FONT_SIZE = 13;
|
|
15
|
+
var LABEL_FONT_WEIGHT = "600";
|
|
16
|
+
var LABEL_MARGIN_BOTTOM = 4;
|
|
17
|
+
var ERROR_FONT_SIZE = 12;
|
|
18
|
+
var ERROR_MARGIN_TOP = 4;
|
|
19
|
+
var styles = reactNative.StyleSheet.create({
|
|
20
|
+
container: {
|
|
21
|
+
marginBottom: CONTAINER_MARGIN_BOTTOM
|
|
22
|
+
},
|
|
23
|
+
label: {
|
|
24
|
+
marginBottom: LABEL_MARGIN_BOTTOM,
|
|
25
|
+
fontSize: LABEL_FONT_SIZE,
|
|
26
|
+
fontWeight: LABEL_FONT_WEIGHT
|
|
27
|
+
},
|
|
28
|
+
errorText: {
|
|
29
|
+
fontSize: ERROR_FONT_SIZE,
|
|
30
|
+
marginTop: ERROR_MARGIN_TOP
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
var fieldSeq = 0;
|
|
34
|
+
function useFieldErrorId() {
|
|
35
|
+
const [id] = React3__default.default.useState(() => {
|
|
36
|
+
fieldSeq += 1;
|
|
37
|
+
return `field-error-${String(fieldSeq)}`;
|
|
38
|
+
});
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
function hasFieldError(error) {
|
|
42
|
+
return typeof error === "string" && error !== "";
|
|
43
|
+
}
|
|
44
|
+
var Field = ({
|
|
45
|
+
label,
|
|
46
|
+
children,
|
|
47
|
+
required = false,
|
|
48
|
+
error,
|
|
49
|
+
containerStyle,
|
|
50
|
+
testID
|
|
51
|
+
}) => {
|
|
52
|
+
const { theme } = uiFeedback.useUi();
|
|
53
|
+
const { colors, semantic } = theme;
|
|
54
|
+
const errorColor = semantic.error["500"];
|
|
55
|
+
const hasError = hasFieldError(error);
|
|
56
|
+
const errorId = useFieldErrorId();
|
|
57
|
+
const themeStyles = React3__default.default.useMemo(
|
|
58
|
+
() => ({
|
|
59
|
+
label: { color: colors.textSecondary },
|
|
60
|
+
requiredMark: { color: errorColor },
|
|
61
|
+
errorText: { color: errorColor }
|
|
62
|
+
}),
|
|
63
|
+
[colors.textSecondary, errorColor]
|
|
64
|
+
);
|
|
65
|
+
const control = typeof children === "function" ? children({ describedById: hasError ? errorId : void 0, hasError }) : children;
|
|
66
|
+
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
|
+
label,
|
|
69
|
+
" ",
|
|
70
|
+
required ? (
|
|
71
|
+
// The asterisk is decorative — the control's `aria-required` conveys "required" to
|
|
72
|
+
// assistive tech, so a screen reader says "required" instead of "star".
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: themeStyles.requiredMark, children: "*" })
|
|
74
|
+
) : null
|
|
75
|
+
] }),
|
|
76
|
+
control,
|
|
77
|
+
hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
|
|
78
|
+
] });
|
|
79
|
+
};
|
|
13
80
|
var IS_WEB = reactNative.Platform.OS === "web";
|
|
14
81
|
var TRANSITION_PROPERTY = "border-color, background-color, box-shadow";
|
|
15
82
|
var TRANSITION_DURATION = "0.14s";
|
|
@@ -24,6 +91,10 @@ var RED_START = 1;
|
|
|
24
91
|
var GREEN_START = 3;
|
|
25
92
|
var BLUE_START = 5;
|
|
26
93
|
var CHANNEL_END_OFFSET = 2;
|
|
94
|
+
function prefersReducedMotion() {
|
|
95
|
+
if (!IS_WEB || typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
96
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
97
|
+
}
|
|
27
98
|
function withAlpha(color, alpha) {
|
|
28
99
|
const hex = color.trim();
|
|
29
100
|
const isHex = hex.charAt(0) === "#" && (hex.length === HEX_FULL_LENGTH || hex.length === HEX_SHORT_LENGTH);
|
|
@@ -68,7 +139,8 @@ function useThemedInput(options = {}) {
|
|
|
68
139
|
const ring = isFocused && !hasError ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(primary, FOCUS_RING_ALPHA)}` : "none";
|
|
69
140
|
const webStyle = {
|
|
70
141
|
transitionProperty: TRANSITION_PROPERTY,
|
|
71
|
-
|
|
142
|
+
// Collapse the eased transition to instant when the user prefers reduced motion.
|
|
143
|
+
transitionDuration: prefersReducedMotion() ? "0s" : TRANSITION_DURATION,
|
|
72
144
|
transitionTimingFunction: TRANSITION_TIMING,
|
|
73
145
|
boxShadow: ring,
|
|
74
146
|
outlineStyle: "none"
|
|
@@ -99,19 +171,26 @@ function useThemedInput(options = {}) {
|
|
|
99
171
|
hoverBind
|
|
100
172
|
};
|
|
101
173
|
}
|
|
102
|
-
var
|
|
174
|
+
var IS_WEB2 = reactNative.Platform.OS === "web";
|
|
175
|
+
var ThemedTextInput = React3__default.default.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
|
|
103
176
|
const {
|
|
104
177
|
style: themedStyle,
|
|
105
178
|
placeholderTextColor: themedPlaceholder,
|
|
106
179
|
focusBind,
|
|
107
180
|
hoverBind
|
|
108
181
|
} = useThemedInput({ hasError, onFocus, onBlur });
|
|
182
|
+
const webA11y = IS_WEB2 ? {
|
|
183
|
+
"aria-invalid": hasError ? true : void 0,
|
|
184
|
+
"aria-describedby": describedById,
|
|
185
|
+
"aria-required": requiredField ? true : void 0
|
|
186
|
+
} : {};
|
|
109
187
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
110
188
|
reactNative.TextInput,
|
|
111
189
|
{
|
|
112
190
|
ref,
|
|
113
191
|
placeholderTextColor: placeholderTextColor ?? themedPlaceholder,
|
|
114
192
|
...rest,
|
|
193
|
+
...webA11y,
|
|
115
194
|
...hoverBind,
|
|
116
195
|
onFocus: focusBind.onFocus,
|
|
117
196
|
onBlur: focusBind.onBlur,
|
|
@@ -119,30 +198,16 @@ var ThemedTextInput = React3__default.default.forwardRef(function ThemedTextInpu
|
|
|
119
198
|
}
|
|
120
199
|
);
|
|
121
200
|
});
|
|
122
|
-
var CONTAINER_MARGIN_BOTTOM = 16;
|
|
123
201
|
var INPUT_PADDING_V = 10;
|
|
124
202
|
var INPUT_PADDING_H = 12;
|
|
125
203
|
var INPUT_BORDER_RADIUS = 8;
|
|
126
|
-
var
|
|
127
|
-
var
|
|
128
|
-
var styles = reactNative.StyleSheet.create({
|
|
129
|
-
container: {
|
|
130
|
-
marginBottom: CONTAINER_MARGIN_BOTTOM
|
|
131
|
-
},
|
|
132
|
-
label: {
|
|
133
|
-
marginBottom: 4,
|
|
134
|
-
fontSize: LABEL_FONT_SIZE,
|
|
135
|
-
fontWeight: "600"
|
|
136
|
-
},
|
|
204
|
+
var INPUT_BORDER_WIDTH = 1;
|
|
205
|
+
var styles2 = reactNative.StyleSheet.create({
|
|
137
206
|
input: {
|
|
138
207
|
paddingVertical: INPUT_PADDING_V,
|
|
139
208
|
paddingHorizontal: INPUT_PADDING_H,
|
|
140
209
|
borderRadius: INPUT_BORDER_RADIUS,
|
|
141
|
-
borderWidth:
|
|
142
|
-
},
|
|
143
|
-
errorText: {
|
|
144
|
-
fontSize: ERROR_FONT_SIZE,
|
|
145
|
-
marginTop: 4
|
|
210
|
+
borderWidth: INPUT_BORDER_WIDTH
|
|
146
211
|
}
|
|
147
212
|
});
|
|
148
213
|
var FormField = ({
|
|
@@ -151,39 +216,19 @@ var FormField = ({
|
|
|
151
216
|
error,
|
|
152
217
|
containerStyle,
|
|
153
218
|
...textInputProps
|
|
154
|
-
}) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.container, containerStyle], children: [
|
|
168
|
-
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles.label, themeStyles.label], children: [
|
|
169
|
-
label,
|
|
170
|
-
" ",
|
|
171
|
-
required ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: themeStyles.requiredMark, children: "*" }) : null
|
|
172
|
-
] }),
|
|
173
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
174
|
-
ThemedTextInput,
|
|
175
|
-
{
|
|
176
|
-
accessibilityHint: `Enter ${label}`,
|
|
177
|
-
accessibilityLabel: label,
|
|
178
|
-
hasError,
|
|
179
|
-
style: styles.input,
|
|
180
|
-
testID: "form-field-input",
|
|
181
|
-
...textInputProps
|
|
182
|
-
}
|
|
183
|
-
),
|
|
184
|
-
hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.errorText, themeStyles.errorText], children: error }) : null
|
|
185
|
-
] });
|
|
186
|
-
};
|
|
219
|
+
}) => /* @__PURE__ */ jsxRuntime.jsx(Field, { containerStyle, error, label, required, children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
220
|
+
ThemedTextInput,
|
|
221
|
+
{
|
|
222
|
+
accessibilityHint: `Enter ${label}`,
|
|
223
|
+
accessibilityLabel: label,
|
|
224
|
+
describedById,
|
|
225
|
+
hasError,
|
|
226
|
+
requiredField: required,
|
|
227
|
+
style: styles2.input,
|
|
228
|
+
testID: "form-field-input",
|
|
229
|
+
...textInputProps
|
|
230
|
+
}
|
|
231
|
+
) });
|
|
187
232
|
var TRANSPARENT_COLOR = "transparent";
|
|
188
233
|
var WHITE_COLOR = "#fff";
|
|
189
234
|
var SOLID_RADIUS = 20;
|
|
@@ -198,6 +243,7 @@ var RGB_R_START = 0;
|
|
|
198
243
|
var RGB_R_END = 2;
|
|
199
244
|
var RGB_G_END = 4;
|
|
200
245
|
var RGB_B_END = 6;
|
|
246
|
+
var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
|
|
201
247
|
function withAlpha2(hex, alpha) {
|
|
202
248
|
const normalized = hex.replace("#", "");
|
|
203
249
|
const isShort = normalized.length === HEX_SHORT_LENGTH2;
|
|
@@ -208,7 +254,7 @@ function withAlpha2(hex, alpha) {
|
|
|
208
254
|
const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
|
|
209
255
|
return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
|
|
210
256
|
}
|
|
211
|
-
var
|
|
257
|
+
var styles3 = reactNative.StyleSheet.create({
|
|
212
258
|
label: {
|
|
213
259
|
marginBottom: 8,
|
|
214
260
|
fontWeight: "600"
|
|
@@ -255,14 +301,15 @@ function OutlineChip({
|
|
|
255
301
|
accessibilityHint: `Selects ${option.label}`,
|
|
256
302
|
accessibilityLabel: option.label,
|
|
257
303
|
accessibilityRole: "button",
|
|
258
|
-
accessibilityState: { selected },
|
|
304
|
+
accessibilityState: { selected, disabled },
|
|
259
305
|
disabled,
|
|
260
|
-
|
|
306
|
+
hitSlop: CHIP_HIT_SLOP,
|
|
307
|
+
style: styles3.chipWrapper,
|
|
261
308
|
testID: `chip-selector-chip-${String(option.value)}`,
|
|
262
309
|
onHoverIn: () => setHovered(true),
|
|
263
310
|
onHoverOut: () => setHovered(false),
|
|
264
311
|
onPress,
|
|
265
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [
|
|
312
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles3.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.chipText, styles3.chipTextOutline, { color: textColor }], children: option.label }) })
|
|
266
313
|
}
|
|
267
314
|
);
|
|
268
315
|
}
|
|
@@ -281,12 +328,13 @@ function SolidChip({
|
|
|
281
328
|
accessibilityHint: `Selects ${option.label}`,
|
|
282
329
|
accessibilityLabel: option.label,
|
|
283
330
|
accessibilityRole: "button",
|
|
284
|
-
accessibilityState: { selected },
|
|
331
|
+
accessibilityState: { selected, disabled },
|
|
285
332
|
disabled,
|
|
286
|
-
|
|
333
|
+
hitSlop: CHIP_HIT_SLOP,
|
|
334
|
+
style: styles3.chipWrapper,
|
|
287
335
|
testID: `chip-selector-chip-${String(option.value)}`,
|
|
288
336
|
onPress,
|
|
289
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [
|
|
337
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles3.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.chipText, { color: textColor }], children: option.label }) })
|
|
290
338
|
}
|
|
291
339
|
);
|
|
292
340
|
}
|
|
@@ -321,8 +369,8 @@ var ChipSelector = ({
|
|
|
321
369
|
return value === optionValue;
|
|
322
370
|
}
|
|
323
371
|
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: containerStyle, children: [
|
|
324
|
-
hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [
|
|
325
|
-
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style:
|
|
372
|
+
hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.label, labelStyle], children: label }) : null,
|
|
373
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.chipContainer, children: options.map((option) => {
|
|
326
374
|
const selected = isSelected(option.value);
|
|
327
375
|
const onPress = () => onChange(option.value);
|
|
328
376
|
return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -350,7 +398,7 @@ var ChipSelector = ({
|
|
|
350
398
|
] });
|
|
351
399
|
};
|
|
352
400
|
var DESCRIPTION_FONT_SIZE = 12;
|
|
353
|
-
var
|
|
401
|
+
var styles4 = reactNative.StyleSheet.create({
|
|
354
402
|
container: {
|
|
355
403
|
flexDirection: "row",
|
|
356
404
|
justifyContent: "space-between",
|
|
@@ -390,13 +438,13 @@ var FormSwitch = ({
|
|
|
390
438
|
reactNative.View,
|
|
391
439
|
{
|
|
392
440
|
style: [
|
|
393
|
-
|
|
441
|
+
styles4.container,
|
|
394
442
|
containerStyle
|
|
395
443
|
],
|
|
396
444
|
children: [
|
|
397
|
-
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style:
|
|
398
|
-
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [
|
|
399
|
-
hasDescription ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [
|
|
445
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.textColumn, children: [
|
|
446
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.label, themeStyles.label], children: label }),
|
|
447
|
+
hasDescription ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.description, themeStyles.description], children: description }) : null
|
|
400
448
|
] }),
|
|
401
449
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
402
450
|
reactNative.Switch,
|
|
@@ -415,12 +463,13 @@ var FormSwitch = ({
|
|
|
415
463
|
}
|
|
416
464
|
);
|
|
417
465
|
};
|
|
466
|
+
var ACTIVATION_KEYS = ["Enter", " ", "Spacebar"];
|
|
418
467
|
var HINT_FONT_SIZE = 12;
|
|
419
468
|
var BOX_SIZE = 20;
|
|
420
469
|
var BOX_RADIUS = 5;
|
|
421
470
|
var CHECK_FONT_SIZE = 14;
|
|
422
471
|
var DISABLED_OPACITY = 0.5;
|
|
423
|
-
var
|
|
472
|
+
var styles5 = reactNative.StyleSheet.create({
|
|
424
473
|
container: {
|
|
425
474
|
flexDirection: "row",
|
|
426
475
|
alignItems: "flex-start",
|
|
@@ -483,6 +532,14 @@ var FormCheckbox = ({
|
|
|
483
532
|
if (onValueChange) onValueChange(next);
|
|
484
533
|
if (onChange) onChange(next);
|
|
485
534
|
};
|
|
535
|
+
const handleKeyDown = (event) => {
|
|
536
|
+
if (disabled) return;
|
|
537
|
+
const key = event.key ?? "";
|
|
538
|
+
if (!ACTIVATION_KEYS.includes(key)) return;
|
|
539
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
540
|
+
toggle();
|
|
541
|
+
};
|
|
542
|
+
const keyboardProps = reactNative.Platform.OS === "web" ? { onKeyDown: handleKeyDown } : {};
|
|
486
543
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
487
544
|
reactNative.Pressable,
|
|
488
545
|
{
|
|
@@ -494,13 +551,14 @@ var FormCheckbox = ({
|
|
|
494
551
|
disabled,
|
|
495
552
|
onPress: toggle,
|
|
496
553
|
role: "checkbox",
|
|
497
|
-
style: [
|
|
554
|
+
style: [styles5.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
|
|
498
555
|
testID,
|
|
556
|
+
...keyboardProps,
|
|
499
557
|
children: [
|
|
500
|
-
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [
|
|
501
|
-
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style:
|
|
502
|
-
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [
|
|
503
|
-
hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [
|
|
558
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
|
|
559
|
+
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles5.textColumn, children: [
|
|
560
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.label, themeStyles.label], children: label }),
|
|
561
|
+
hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.hint, themeStyles.hint], children: hintText }) : null
|
|
504
562
|
] })
|
|
505
563
|
]
|
|
506
564
|
}
|
|
@@ -508,6 +566,7 @@ var FormCheckbox = ({
|
|
|
508
566
|
};
|
|
509
567
|
|
|
510
568
|
exports.ChipSelector = ChipSelector;
|
|
569
|
+
exports.Field = Field;
|
|
511
570
|
exports.FormCheckbox = FormCheckbox;
|
|
512
571
|
exports.FormField = FormField;
|
|
513
572
|
exports.FormSwitch = FormSwitch;
|