@dloizides/ui-forms 1.2.0 → 1.4.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 +28 -0
- package/dist/index.d.mts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +170 -54
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
- **Accessibility (WCAG 2.1 AA) hardening — additive + backward-compatible.**
|
|
6
|
+
- `FormField`: the error message is now programmatically tied to the input — the input gets
|
|
7
|
+
`aria-invalid` when errored and `aria-describedby` pointing at the error line (which is a
|
|
8
|
+
`role="alert"` live region), and `aria-required` when `required`. The required `*` is marked
|
|
9
|
+
decorative (`aria-hidden` / `accessibilityElementsHidden`) so a screen reader hears "required"
|
|
10
|
+
(via `aria-required`) instead of "star".
|
|
11
|
+
- `ThemedTextInput`: new optional `describedById` + `requiredField` props forward
|
|
12
|
+
`aria-describedby` / `aria-required`, and it emits `aria-invalid` from `hasError` (web only;
|
|
13
|
+
no-ops on native). Existing callers are unaffected.
|
|
14
|
+
- `FormCheckbox`: now keyboard-operable — the `role="checkbox"` Pressable renders a `<div>`
|
|
15
|
+
(no free Enter/Space), so an explicit web-only Enter/Space handler toggles it (WCAG 2.1.1).
|
|
16
|
+
- `ChipSelector`: chips gain a `hitSlop` so the touch target reaches ~44px WITHOUT changing the
|
|
17
|
+
rendered pill size, and expose `accessibilityState.disabled` when the selector is disabled.
|
|
18
|
+
- `useThemedInput`: the focus/hover CSS transition now collapses to instant when the user
|
|
19
|
+
requests `prefers-reduced-motion: reduce` (WCAG 2.3.3, web only).
|
|
20
|
+
|
|
21
|
+
## 1.3.0
|
|
22
|
+
|
|
23
|
+
- `ChipSelector` gains a `variant` prop (`'solid' | 'outline'`, default `'solid'`). `solid` is the
|
|
24
|
+
original filled-pill look (selected = solid brand fill, white ink) — existing consumers
|
|
25
|
+
(erevna / katalogos / kefi) render **identically**, no change. `outline` is the AML v1 console
|
|
26
|
+
look: an outlined pill on a muted fill where the **selected** state is a subtle tinted-outline
|
|
27
|
+
(brand border + brand ink + a low-alpha brand wash), NOT a solid fill; on web, hover lifts the
|
|
28
|
+
border + ink to the brand colour. Fully theme-driven (brand colour + alpha, border/text from the
|
|
29
|
+
theme), so it re-themes per tenant. Additive + backward-compatible.
|
|
30
|
+
|
|
3
31
|
## 1.2.0
|
|
4
32
|
|
|
5
33
|
- Add `ThemedTextInput` + `useThemedInput` — the shared "readable + lively" text-input treatment,
|
package/dist/index.d.mts
CHANGED
|
@@ -33,6 +33,13 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
|
|
|
33
33
|
interface ThemedTextInputProps extends TextInputProps {
|
|
34
34
|
/** Render with the error border colour (matches `FormField`'s error treatment). */
|
|
35
35
|
hasError?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* id of an element describing this input (e.g. an error/hint line). Linked via
|
|
38
|
+
* `aria-describedby` on web so a screen reader reads it after the field's name. No-op native.
|
|
39
|
+
*/
|
|
40
|
+
describedById?: string;
|
|
41
|
+
/** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
|
|
42
|
+
requiredField?: boolean;
|
|
36
43
|
}
|
|
37
44
|
declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
|
|
38
45
|
|
|
@@ -66,8 +73,17 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
|
|
|
66
73
|
|
|
67
74
|
/**
|
|
68
75
|
* Chip/pill selector for selecting one or multiple options.
|
|
76
|
+
*
|
|
77
|
+
* Two visual variants (both fully theme-driven):
|
|
78
|
+
* - `solid` (default) — the original filled-pill look: selected = solid primary fill with
|
|
79
|
+
* white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
|
|
80
|
+
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
81
|
+
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
82
|
+
* Hover (web) lifts the border/ink to the primary colour.
|
|
69
83
|
*/
|
|
70
84
|
|
|
85
|
+
/** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
|
|
86
|
+
type ChipVariant = 'solid' | 'outline';
|
|
71
87
|
interface ChipOption<T> {
|
|
72
88
|
value: T;
|
|
73
89
|
label: string;
|
|
@@ -79,9 +95,11 @@ interface ChipSelectorProps<T> {
|
|
|
79
95
|
onChange: (value: T) => void;
|
|
80
96
|
multiple?: boolean;
|
|
81
97
|
disabled?: boolean;
|
|
98
|
+
/** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
|
|
99
|
+
variant?: ChipVariant;
|
|
82
100
|
containerStyle?: ViewStyle;
|
|
83
101
|
}
|
|
84
|
-
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
102
|
+
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
85
103
|
|
|
86
104
|
/**
|
|
87
105
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -124,4 +142,4 @@ interface FormCheckboxProps {
|
|
|
124
142
|
}
|
|
125
143
|
declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
|
|
126
144
|
|
|
127
|
-
export { type ChipOption, ChipSelector, type ChipSelectorProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,13 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
|
|
|
33
33
|
interface ThemedTextInputProps extends TextInputProps {
|
|
34
34
|
/** Render with the error border colour (matches `FormField`'s error treatment). */
|
|
35
35
|
hasError?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* id of an element describing this input (e.g. an error/hint line). Linked via
|
|
38
|
+
* `aria-describedby` on web so a screen reader reads it after the field's name. No-op native.
|
|
39
|
+
*/
|
|
40
|
+
describedById?: string;
|
|
41
|
+
/** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
|
|
42
|
+
requiredField?: boolean;
|
|
36
43
|
}
|
|
37
44
|
declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
|
|
38
45
|
|
|
@@ -66,8 +73,17 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
|
|
|
66
73
|
|
|
67
74
|
/**
|
|
68
75
|
* Chip/pill selector for selecting one or multiple options.
|
|
76
|
+
*
|
|
77
|
+
* Two visual variants (both fully theme-driven):
|
|
78
|
+
* - `solid` (default) — the original filled-pill look: selected = solid primary fill with
|
|
79
|
+
* white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
|
|
80
|
+
* - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
|
|
81
|
+
* tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
|
|
82
|
+
* Hover (web) lifts the border/ink to the primary colour.
|
|
69
83
|
*/
|
|
70
84
|
|
|
85
|
+
/** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
|
|
86
|
+
type ChipVariant = 'solid' | 'outline';
|
|
71
87
|
interface ChipOption<T> {
|
|
72
88
|
value: T;
|
|
73
89
|
label: string;
|
|
@@ -79,9 +95,11 @@ interface ChipSelectorProps<T> {
|
|
|
79
95
|
onChange: (value: T) => void;
|
|
80
96
|
multiple?: boolean;
|
|
81
97
|
disabled?: boolean;
|
|
98
|
+
/** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
|
|
99
|
+
variant?: ChipVariant;
|
|
82
100
|
containerStyle?: ViewStyle;
|
|
83
101
|
}
|
|
84
|
-
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
102
|
+
declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
|
|
85
103
|
|
|
86
104
|
/**
|
|
87
105
|
* Reusable form switch/toggle with an optional description.
|
|
@@ -124,4 +142,4 @@ interface FormCheckboxProps {
|
|
|
124
142
|
}
|
|
125
143
|
declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
|
|
126
144
|
|
|
127
|
-
export { type ChipOption, ChipSelector, type ChipSelectorProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React3 = 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 React3__default = /*#__PURE__*/_interopDefault(React3);
|
|
11
11
|
|
|
12
12
|
// src/FormField/FormField.tsx
|
|
13
13
|
var IS_WEB = reactNative.Platform.OS === "web";
|
|
@@ -24,6 +24,10 @@ var RED_START = 1;
|
|
|
24
24
|
var GREEN_START = 3;
|
|
25
25
|
var BLUE_START = 5;
|
|
26
26
|
var CHANNEL_END_OFFSET = 2;
|
|
27
|
+
function prefersReducedMotion() {
|
|
28
|
+
if (!IS_WEB || typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
|
|
29
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
30
|
+
}
|
|
27
31
|
function withAlpha(color, alpha) {
|
|
28
32
|
const hex = color.trim();
|
|
29
33
|
const isHex = hex.charAt(0) === "#" && (hex.length === HEX_FULL_LENGTH || hex.length === HEX_SHORT_LENGTH);
|
|
@@ -38,27 +42,27 @@ function useThemedInput(options = {}) {
|
|
|
38
42
|
const { hasError = false, onFocus, onBlur } = options;
|
|
39
43
|
const { theme } = uiFeedback.useUi();
|
|
40
44
|
const { colors, palette, semantic } = theme;
|
|
41
|
-
const [isFocused, setIsFocused] =
|
|
42
|
-
const [isHovered, setIsHovered] =
|
|
43
|
-
const handleFocus =
|
|
45
|
+
const [isFocused, setIsFocused] = React3.useState(false);
|
|
46
|
+
const [isHovered, setIsHovered] = React3.useState(false);
|
|
47
|
+
const handleFocus = React3.useCallback(
|
|
44
48
|
(e) => {
|
|
45
49
|
setIsFocused(true);
|
|
46
50
|
onFocus?.(e);
|
|
47
51
|
},
|
|
48
52
|
[onFocus]
|
|
49
53
|
);
|
|
50
|
-
const handleBlur =
|
|
54
|
+
const handleBlur = React3.useCallback(
|
|
51
55
|
(e) => {
|
|
52
56
|
setIsFocused(false);
|
|
53
57
|
onBlur?.(e);
|
|
54
58
|
},
|
|
55
59
|
[onBlur]
|
|
56
60
|
);
|
|
57
|
-
const handleHoverIn =
|
|
58
|
-
const handleHoverOut =
|
|
61
|
+
const handleHoverIn = React3.useCallback(() => setIsHovered(true), []);
|
|
62
|
+
const handleHoverOut = React3.useCallback(() => setIsHovered(false), []);
|
|
59
63
|
const primary = palette.primary["500"];
|
|
60
64
|
const errorColor = semantic.error["500"];
|
|
61
|
-
const style =
|
|
65
|
+
const style = React3.useMemo(() => {
|
|
62
66
|
const restBorder = hasError ? errorColor : colors.border;
|
|
63
67
|
const focusBorder = hasError ? errorColor : primary;
|
|
64
68
|
const borderColor = isFocused ? focusBorder : isHovered ? withAlpha(colors.text, HOVER_BORDER_ALPHA) : restBorder;
|
|
@@ -68,7 +72,8 @@ function useThemedInput(options = {}) {
|
|
|
68
72
|
const ring = isFocused && !hasError ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(primary, FOCUS_RING_ALPHA)}` : "none";
|
|
69
73
|
const webStyle = {
|
|
70
74
|
transitionProperty: TRANSITION_PROPERTY,
|
|
71
|
-
|
|
75
|
+
// Collapse the eased transition to instant when the user prefers reduced motion.
|
|
76
|
+
transitionDuration: prefersReducedMotion() ? "0s" : TRANSITION_DURATION,
|
|
72
77
|
transitionTimingFunction: TRANSITION_TIMING,
|
|
73
78
|
boxShadow: ring,
|
|
74
79
|
outlineStyle: "none"
|
|
@@ -85,8 +90,8 @@ function useThemedInput(options = {}) {
|
|
|
85
90
|
primary,
|
|
86
91
|
errorColor
|
|
87
92
|
]);
|
|
88
|
-
const focusBind =
|
|
89
|
-
const hoverBind =
|
|
93
|
+
const focusBind = React3.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
|
|
94
|
+
const hoverBind = React3.useMemo(
|
|
90
95
|
() => IS_WEB ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
|
|
91
96
|
[handleHoverIn, handleHoverOut]
|
|
92
97
|
);
|
|
@@ -99,19 +104,26 @@ function useThemedInput(options = {}) {
|
|
|
99
104
|
hoverBind
|
|
100
105
|
};
|
|
101
106
|
}
|
|
102
|
-
var
|
|
107
|
+
var IS_WEB2 = reactNative.Platform.OS === "web";
|
|
108
|
+
var ThemedTextInput = React3__default.default.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
|
|
103
109
|
const {
|
|
104
110
|
style: themedStyle,
|
|
105
111
|
placeholderTextColor: themedPlaceholder,
|
|
106
112
|
focusBind,
|
|
107
113
|
hoverBind
|
|
108
114
|
} = useThemedInput({ hasError, onFocus, onBlur });
|
|
115
|
+
const webA11y = IS_WEB2 ? {
|
|
116
|
+
"aria-invalid": hasError ? true : void 0,
|
|
117
|
+
"aria-describedby": describedById,
|
|
118
|
+
"aria-required": requiredField ? true : void 0
|
|
119
|
+
} : {};
|
|
109
120
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
110
121
|
reactNative.TextInput,
|
|
111
122
|
{
|
|
112
123
|
ref,
|
|
113
124
|
placeholderTextColor: placeholderTextColor ?? themedPlaceholder,
|
|
114
125
|
...rest,
|
|
126
|
+
...webA11y,
|
|
115
127
|
...hoverBind,
|
|
116
128
|
onFocus: focusBind.onFocus,
|
|
117
129
|
onBlur: focusBind.onBlur,
|
|
@@ -145,6 +157,14 @@ var styles = reactNative.StyleSheet.create({
|
|
|
145
157
|
marginTop: 4
|
|
146
158
|
}
|
|
147
159
|
});
|
|
160
|
+
var formFieldSeq = 0;
|
|
161
|
+
function useErrorId() {
|
|
162
|
+
const [id] = React3__default.default.useState(() => {
|
|
163
|
+
formFieldSeq += 1;
|
|
164
|
+
return `form-field-error-${String(formFieldSeq)}`;
|
|
165
|
+
});
|
|
166
|
+
return id;
|
|
167
|
+
}
|
|
148
168
|
var FormField = ({
|
|
149
169
|
label,
|
|
150
170
|
required = false,
|
|
@@ -156,7 +176,8 @@ var FormField = ({
|
|
|
156
176
|
const { colors, semantic } = theme;
|
|
157
177
|
const errorColor = semantic.error["500"];
|
|
158
178
|
const hasError = typeof error === "string" && error !== "";
|
|
159
|
-
const
|
|
179
|
+
const errorId = useErrorId();
|
|
180
|
+
const themeStyles = React3__default.default.useMemo(
|
|
160
181
|
() => ({
|
|
161
182
|
label: { color: colors.textSecondary },
|
|
162
183
|
requiredMark: { color: errorColor },
|
|
@@ -168,24 +189,52 @@ var FormField = ({
|
|
|
168
189
|
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles.label, themeStyles.label], children: [
|
|
169
190
|
label,
|
|
170
191
|
" ",
|
|
171
|
-
required ?
|
|
192
|
+
required ? (
|
|
193
|
+
// The asterisk is decorative — `requiredField` conveys "required" to assistive tech.
|
|
194
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: themeStyles.requiredMark, children: "*" })
|
|
195
|
+
) : null
|
|
172
196
|
] }),
|
|
173
197
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
174
198
|
ThemedTextInput,
|
|
175
199
|
{
|
|
176
200
|
accessibilityHint: `Enter ${label}`,
|
|
177
201
|
accessibilityLabel: label,
|
|
202
|
+
describedById: hasError ? errorId : void 0,
|
|
178
203
|
hasError,
|
|
204
|
+
requiredField: required,
|
|
179
205
|
style: styles.input,
|
|
180
206
|
testID: "form-field-input",
|
|
181
207
|
...textInputProps
|
|
182
208
|
}
|
|
183
209
|
),
|
|
184
|
-
hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.errorText, themeStyles.errorText], children: error }) : null
|
|
210
|
+
hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
|
|
185
211
|
] });
|
|
186
212
|
};
|
|
187
213
|
var TRANSPARENT_COLOR = "transparent";
|
|
188
214
|
var WHITE_COLOR = "#fff";
|
|
215
|
+
var SOLID_RADIUS = 20;
|
|
216
|
+
var OUTLINE_RADIUS = 999;
|
|
217
|
+
var OUTLINE_PAD_H = 12;
|
|
218
|
+
var OUTLINE_PAD_V = 5;
|
|
219
|
+
var OUTLINE_FONT_SIZE = 13;
|
|
220
|
+
var OUTLINE_TINT_ALPHA = 0.1;
|
|
221
|
+
var HEX_SHORT_LENGTH2 = 3;
|
|
222
|
+
var HEX_RADIX2 = 16;
|
|
223
|
+
var RGB_R_START = 0;
|
|
224
|
+
var RGB_R_END = 2;
|
|
225
|
+
var RGB_G_END = 4;
|
|
226
|
+
var RGB_B_END = 6;
|
|
227
|
+
var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
|
|
228
|
+
function withAlpha2(hex, alpha) {
|
|
229
|
+
const normalized = hex.replace("#", "");
|
|
230
|
+
const isShort = normalized.length === HEX_SHORT_LENGTH2;
|
|
231
|
+
const full = isShort ? normalized.split("").map((c) => c + c).join("") : normalized;
|
|
232
|
+
const r = parseInt(full.slice(RGB_R_START, RGB_R_END), HEX_RADIX2);
|
|
233
|
+
const g = parseInt(full.slice(RGB_R_END, RGB_G_END), HEX_RADIX2);
|
|
234
|
+
const b = parseInt(full.slice(RGB_G_END, RGB_B_END), HEX_RADIX2);
|
|
235
|
+
const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
|
|
236
|
+
return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
|
|
237
|
+
}
|
|
189
238
|
var styles2 = reactNative.StyleSheet.create({
|
|
190
239
|
label: {
|
|
191
240
|
marginBottom: 8,
|
|
@@ -202,12 +251,74 @@ var styles2 = reactNative.StyleSheet.create({
|
|
|
202
251
|
chip: {
|
|
203
252
|
paddingHorizontal: 12,
|
|
204
253
|
paddingVertical: 6,
|
|
205
|
-
borderRadius:
|
|
254
|
+
borderRadius: SOLID_RADIUS,
|
|
206
255
|
backgroundColor: TRANSPARENT_COLOR,
|
|
207
256
|
borderWidth: 1
|
|
208
257
|
},
|
|
209
|
-
|
|
258
|
+
chipOutline: {
|
|
259
|
+
paddingHorizontal: OUTLINE_PAD_H,
|
|
260
|
+
paddingVertical: OUTLINE_PAD_V,
|
|
261
|
+
borderRadius: OUTLINE_RADIUS,
|
|
262
|
+
borderWidth: 1
|
|
263
|
+
},
|
|
264
|
+
chipText: {},
|
|
265
|
+
chipTextOutline: { fontSize: OUTLINE_FONT_SIZE }
|
|
210
266
|
});
|
|
267
|
+
function OutlineChip({
|
|
268
|
+
option,
|
|
269
|
+
selected,
|
|
270
|
+
disabled,
|
|
271
|
+
colors,
|
|
272
|
+
onPress
|
|
273
|
+
}) {
|
|
274
|
+
const [hovered, setHovered] = React3__default.default.useState(false);
|
|
275
|
+
const active = selected || hovered;
|
|
276
|
+
const borderColor = active ? colors.primary : colors.border;
|
|
277
|
+
const backgroundColor = selected ? colors.tint : colors.muted;
|
|
278
|
+
const textColor = active ? colors.primary : colors.textMuted;
|
|
279
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
280
|
+
reactNative.Pressable,
|
|
281
|
+
{
|
|
282
|
+
accessibilityHint: `Selects ${option.label}`,
|
|
283
|
+
accessibilityLabel: option.label,
|
|
284
|
+
accessibilityRole: "button",
|
|
285
|
+
accessibilityState: { selected, disabled },
|
|
286
|
+
disabled,
|
|
287
|
+
hitSlop: CHIP_HIT_SLOP,
|
|
288
|
+
style: styles2.chipWrapper,
|
|
289
|
+
testID: `chip-selector-chip-${String(option.value)}`,
|
|
290
|
+
onHoverIn: () => setHovered(true),
|
|
291
|
+
onHoverOut: () => setHovered(false),
|
|
292
|
+
onPress,
|
|
293
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.chipText, styles2.chipTextOutline, { color: textColor }], children: option.label }) })
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
function SolidChip({
|
|
298
|
+
option,
|
|
299
|
+
selected,
|
|
300
|
+
disabled,
|
|
301
|
+
colors,
|
|
302
|
+
onPress
|
|
303
|
+
}) {
|
|
304
|
+
const backgroundColor = selected ? colors.primary : TRANSPARENT_COLOR;
|
|
305
|
+
const textColor = selected ? WHITE_COLOR : colors.textStrong;
|
|
306
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
307
|
+
reactNative.TouchableOpacity,
|
|
308
|
+
{
|
|
309
|
+
accessibilityHint: `Selects ${option.label}`,
|
|
310
|
+
accessibilityLabel: option.label,
|
|
311
|
+
accessibilityRole: "button",
|
|
312
|
+
accessibilityState: { selected, disabled },
|
|
313
|
+
disabled,
|
|
314
|
+
hitSlop: CHIP_HIT_SLOP,
|
|
315
|
+
style: styles2.chipWrapper,
|
|
316
|
+
testID: `chip-selector-chip-${String(option.value)}`,
|
|
317
|
+
onPress,
|
|
318
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.chipText, { color: textColor }], children: option.label }) })
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
}
|
|
211
322
|
var ChipSelector = ({
|
|
212
323
|
label,
|
|
213
324
|
options,
|
|
@@ -215,57 +326,52 @@ var ChipSelector = ({
|
|
|
215
326
|
onChange,
|
|
216
327
|
multiple = false,
|
|
217
328
|
disabled = false,
|
|
329
|
+
variant = "solid",
|
|
218
330
|
containerStyle
|
|
219
331
|
}) => {
|
|
220
332
|
const { theme } = uiFeedback.useUi();
|
|
221
333
|
const { colors, palette } = theme;
|
|
222
|
-
const primaryColor = palette.primary["500"];
|
|
223
334
|
const hasLabel = typeof label === "string" && label !== "";
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const chipStyle = { borderColor: colors.border };
|
|
227
|
-
const chipSelectedStyle = { backgroundColor: primaryColor };
|
|
228
|
-
const chipTextStyle = { color: colors.text };
|
|
229
|
-
const chipTextSelectedStyle = { color: WHITE_COLOR };
|
|
335
|
+
const chipColors = React3__default.default.useMemo(() => {
|
|
336
|
+
const primary = palette.primary["500"];
|
|
230
337
|
return {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
338
|
+
border: colors.border,
|
|
339
|
+
textStrong: colors.text,
|
|
340
|
+
textMuted: colors.textSecondary,
|
|
341
|
+
primary,
|
|
342
|
+
muted: colors.background,
|
|
343
|
+
tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
|
|
236
344
|
};
|
|
237
|
-
}, [colors.border, colors.text,
|
|
345
|
+
}, [colors.border, colors.text, colors.textSecondary, colors.background, palette.primary]);
|
|
346
|
+
const labelStyle = React3__default.default.useMemo(() => ({ color: colors.text }), [colors.text]);
|
|
238
347
|
function isSelected(optionValue) {
|
|
239
348
|
if (multiple && Array.isArray(value))
|
|
240
349
|
return value.includes(optionValue);
|
|
241
350
|
return value === optionValue;
|
|
242
351
|
}
|
|
243
352
|
return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: containerStyle, children: [
|
|
244
|
-
hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.label,
|
|
353
|
+
hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.label, labelStyle], children: label }) : null,
|
|
245
354
|
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.chipContainer, children: options.map((option) => {
|
|
246
355
|
const selected = isSelected(option.value);
|
|
247
|
-
|
|
248
|
-
|
|
356
|
+
const onPress = () => onChange(option.value);
|
|
357
|
+
return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
358
|
+
OutlineChip,
|
|
359
|
+
{
|
|
360
|
+
colors: chipColors,
|
|
361
|
+
disabled,
|
|
362
|
+
option,
|
|
363
|
+
selected,
|
|
364
|
+
onPress
|
|
365
|
+
},
|
|
366
|
+
String(option.value)
|
|
367
|
+
) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
368
|
+
SolidChip,
|
|
249
369
|
{
|
|
250
|
-
|
|
251
|
-
accessibilityLabel: option.label,
|
|
252
|
-
accessibilityRole: "button",
|
|
253
|
-
accessibilityState: { selected },
|
|
370
|
+
colors: chipColors,
|
|
254
371
|
disabled,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
onPress
|
|
258
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
259
|
-
reactNative.View,
|
|
260
|
-
{
|
|
261
|
-
style: [
|
|
262
|
-
styles2.chip,
|
|
263
|
-
themeStyles.chip,
|
|
264
|
-
selected ? themeStyles.chipSelected : null
|
|
265
|
-
],
|
|
266
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.chipText, themeStyles.chipText, selected ? themeStyles.chipTextSelected : null], children: option.label })
|
|
267
|
-
}
|
|
268
|
-
)
|
|
372
|
+
option,
|
|
373
|
+
selected,
|
|
374
|
+
onPress
|
|
269
375
|
},
|
|
270
376
|
String(option.value)
|
|
271
377
|
);
|
|
@@ -304,7 +410,7 @@ var FormSwitch = ({
|
|
|
304
410
|
const { theme } = uiFeedback.useUi();
|
|
305
411
|
const { colors } = theme;
|
|
306
412
|
const hasDescription = typeof description === "string" && description !== "";
|
|
307
|
-
const themeStyles =
|
|
413
|
+
const themeStyles = React3__default.default.useMemo(() => {
|
|
308
414
|
const labelStyle = { color: colors.text };
|
|
309
415
|
const descriptionStyle = { color: colors.textSecondary };
|
|
310
416
|
return { label: labelStyle, description: descriptionStyle };
|
|
@@ -338,6 +444,7 @@ var FormSwitch = ({
|
|
|
338
444
|
}
|
|
339
445
|
);
|
|
340
446
|
};
|
|
447
|
+
var ACTIVATION_KEYS = ["Enter", " ", "Spacebar"];
|
|
341
448
|
var HINT_FONT_SIZE = 12;
|
|
342
449
|
var BOX_SIZE = 20;
|
|
343
450
|
var BOX_RADIUS = 5;
|
|
@@ -394,7 +501,7 @@ var FormCheckbox = ({
|
|
|
394
501
|
const brand = palette.primary["500"];
|
|
395
502
|
const hintText = typeof hint === "string" && hint !== "" ? hint : description;
|
|
396
503
|
const hasHint = typeof hintText === "string" && hintText !== "";
|
|
397
|
-
const themeStyles =
|
|
504
|
+
const themeStyles = React3__default.default.useMemo(() => {
|
|
398
505
|
const boxOn = { backgroundColor: brand, borderColor: brand };
|
|
399
506
|
const boxOff = { backgroundColor: "transparent", borderColor: colors.border };
|
|
400
507
|
const labelStyle = { color: colors.text };
|
|
@@ -406,6 +513,14 @@ var FormCheckbox = ({
|
|
|
406
513
|
if (onValueChange) onValueChange(next);
|
|
407
514
|
if (onChange) onChange(next);
|
|
408
515
|
};
|
|
516
|
+
const handleKeyDown = (event) => {
|
|
517
|
+
if (disabled) return;
|
|
518
|
+
const key = event.key ?? "";
|
|
519
|
+
if (!ACTIVATION_KEYS.includes(key)) return;
|
|
520
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
521
|
+
toggle();
|
|
522
|
+
};
|
|
523
|
+
const keyboardProps = reactNative.Platform.OS === "web" ? { onKeyDown: handleKeyDown } : {};
|
|
409
524
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
410
525
|
reactNative.Pressable,
|
|
411
526
|
{
|
|
@@ -419,6 +534,7 @@ var FormCheckbox = ({
|
|
|
419
534
|
role: "checkbox",
|
|
420
535
|
style: [styles4.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
|
|
421
536
|
testID,
|
|
537
|
+
...keyboardProps,
|
|
422
538
|
children: [
|
|
423
539
|
/* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles4.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
|
|
424
540
|
/* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.textColumn, children: [
|