@dloizides/ui-forms 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,51 +1,122 @@
1
1
  'use strict';
2
2
 
3
- var React3 = require('react');
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
+ var uiButtons = require('@dloizides/ui-buttons');
7
8
 
8
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
10
 
10
- var React3__default = /*#__PURE__*/_interopDefault(React3);
11
+ var React__default = /*#__PURE__*/_interopDefault(React);
11
12
 
12
13
  // src/Field/Field.tsx
14
+ function FieldLabel({
15
+ label,
16
+ marker,
17
+ optionalLabel,
18
+ style,
19
+ requiredMarkStyle,
20
+ optionalMarkStyle
21
+ }) {
22
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style, children: [
23
+ label,
24
+ " ",
25
+ marker === "required" ? (
26
+ // The asterisk is decorative — the control's `aria-required` conveys "required" to
27
+ // assistive tech, so a screen reader says "required" instead of "star".
28
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: requiredMarkStyle, children: "*" })
29
+ ) : null,
30
+ marker === "optional" && optionalLabel !== "" ? (
31
+ // Also decorative: "optional" is the ABSENCE of `aria-required`, which assistive tech
32
+ // already conveys. Rendering it to a screen reader would double-announce the same fact.
33
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: optionalMarkStyle, children: optionalLabel })
34
+ ) : null
35
+ ] });
36
+ }
37
+
38
+ // src/Field/fieldMarkers.ts
39
+ var DEFAULT_OPTIONAL_LABEL = "(optional)";
40
+ function resolveFieldMarker(required, optional) {
41
+ if (required) return "required";
42
+ if (optional) return "optional";
43
+ return "none";
44
+ }
45
+ var FIELD_FONT_SIZE = 13;
46
+ var FIELD_FONT_WEIGHT = "600";
47
+ var CONTROL_FONT_SIZE = 11;
48
+ var CONTROL_FONT_WEIGHT = "700";
49
+ var CONTROL_LETTER_SPACING = 0.4;
50
+ var labelVariantStyles = reactNative.StyleSheet.create({
51
+ field: {
52
+ fontSize: FIELD_FONT_SIZE,
53
+ fontWeight: FIELD_FONT_WEIGHT
54
+ },
55
+ control: {
56
+ fontSize: CONTROL_FONT_SIZE,
57
+ fontWeight: CONTROL_FONT_WEIGHT,
58
+ letterSpacing: CONTROL_LETTER_SPACING,
59
+ textTransform: "uppercase"
60
+ }
61
+ });
62
+ function resolveLabelVariantStyle(variant) {
63
+ return variant === "control" ? labelVariantStyles.control : labelVariantStyles.field;
64
+ }
13
65
  var CONTAINER_MARGIN_BOTTOM = 16;
14
- var LABEL_FONT_SIZE = 13;
15
- var LABEL_FONT_WEIGHT = "600";
16
66
  var LABEL_MARGIN_BOTTOM = 4;
17
67
  var ERROR_FONT_SIZE = 12;
18
68
  var ERROR_MARGIN_TOP = 4;
69
+ var HINT_FONT_SIZE = 12;
70
+ var HINT_MARGIN_TOP = 4;
71
+ var OPTIONAL_MARK_FONT_WEIGHT = "400";
19
72
  var styles = reactNative.StyleSheet.create({
20
73
  container: {
21
74
  marginBottom: CONTAINER_MARGIN_BOTTOM
22
75
  },
23
76
  label: {
24
- marginBottom: LABEL_MARGIN_BOTTOM,
25
- fontSize: LABEL_FONT_SIZE,
26
- fontWeight: LABEL_FONT_WEIGHT
77
+ marginBottom: LABEL_MARGIN_BOTTOM
78
+ },
79
+ hintText: {
80
+ fontSize: HINT_FONT_SIZE,
81
+ marginTop: HINT_MARGIN_TOP
27
82
  },
28
83
  errorText: {
29
84
  fontSize: ERROR_FONT_SIZE,
30
85
  marginTop: ERROR_MARGIN_TOP
86
+ },
87
+ optionalMark: {
88
+ fontWeight: OPTIONAL_MARK_FONT_WEIGHT
31
89
  }
32
90
  });
33
91
  var fieldSeq = 0;
34
- function useFieldErrorId() {
35
- const [id] = React3__default.default.useState(() => {
92
+ function useFieldIds() {
93
+ const [ids] = React__default.default.useState(() => {
36
94
  fieldSeq += 1;
37
- return `field-error-${String(fieldSeq)}`;
95
+ const seq = String(fieldSeq);
96
+ return { hintId: `field-hint-${seq}`, errorId: `field-error-${seq}` };
38
97
  });
39
- return id;
98
+ return ids;
99
+ }
100
+ function isPresent(value) {
101
+ return typeof value === "string" && value !== "";
40
102
  }
41
103
  function hasFieldError(error) {
42
- return typeof error === "string" && error !== "";
104
+ return isPresent(error);
105
+ }
106
+ function joinDescribedBy(hintId, errorId) {
107
+ const ids = [hintId, errorId].filter((id) => isPresent(id));
108
+ return ids.length > 0 ? ids.join(" ") : void 0;
43
109
  }
44
110
  var Field = ({
45
111
  label,
46
112
  children,
47
113
  required = false,
114
+ optional = false,
115
+ optionalLabel = DEFAULT_OPTIONAL_LABEL,
116
+ hint,
48
117
  error,
118
+ labelVariant,
119
+ labelStyle,
49
120
  containerStyle,
50
121
  testID
51
122
  }) => {
@@ -53,30 +124,89 @@ var Field = ({
53
124
  const { colors, semantic } = theme;
54
125
  const errorColor = semantic.error["500"];
55
126
  const hasError = hasFieldError(error);
56
- const errorId = useFieldErrorId();
57
- const themeStyles = React3__default.default.useMemo(
127
+ const hasHint = isPresent(hint);
128
+ const labelText = label ?? "";
129
+ const hasLabel = labelText !== "";
130
+ const marker = resolveFieldMarker(required, optional);
131
+ const { hintId, errorId } = useFieldIds();
132
+ const themeStyles = React__default.default.useMemo(
58
133
  () => ({
59
134
  label: { color: colors.textSecondary },
60
135
  requiredMark: { color: errorColor },
136
+ // The optional marker is an aside, so it sits a step back from the label's own ink.
137
+ optionalMark: { color: colors.textSecondary },
138
+ hintText: { color: colors.textSecondary },
61
139
  errorText: { color: errorColor }
62
140
  }),
63
141
  [colors.textSecondary, errorColor]
64
142
  );
65
- const control = typeof children === "function" ? children({ describedById: hasError ? errorId : void 0, hasError }) : children;
143
+ const describedById = joinDescribedBy(hasHint ? hintId : void 0, hasError ? errorId : void 0);
144
+ const control = typeof children === "function" ? children({ describedById, hasError }) : children;
66
145
  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
- ] }),
146
+ hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(
147
+ FieldLabel,
148
+ {
149
+ label: labelText,
150
+ marker,
151
+ optionalLabel,
152
+ optionalMarkStyle: [styles.optionalMark, themeStyles.optionalMark],
153
+ requiredMarkStyle: themeStyles.requiredMark,
154
+ style: [styles.label, resolveLabelVariantStyle(labelVariant), themeStyles.label, labelStyle]
155
+ }
156
+ ) : null,
76
157
  control,
158
+ hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: hintId, style: [styles.hintText, themeStyles.hintText], children: hint }) : null,
77
159
  hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
78
160
  ] });
79
161
  };
162
+ var ACTIONS_GAP = 12;
163
+ var styles2 = reactNative.StyleSheet.create({
164
+ container: {
165
+ flexDirection: "row",
166
+ justifyContent: "flex-end",
167
+ gap: ACTIONS_GAP
168
+ }
169
+ });
170
+ var FormActions = ({
171
+ onSave,
172
+ onCancel,
173
+ saveLabel,
174
+ cancelLabel = "",
175
+ saveHint,
176
+ cancelHint,
177
+ saving = false,
178
+ saveDisabled = false,
179
+ containerStyle,
180
+ saveTestID = "save-button",
181
+ cancelTestID = "cancel-button",
182
+ testID
183
+ }) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles2.container, containerStyle], testID, children: [
184
+ typeof onCancel === "function" ? /* @__PURE__ */ jsxRuntime.jsx(
185
+ uiButtons.Button,
186
+ {
187
+ accessibilityHint: cancelHint ?? cancelLabel,
188
+ accessibilityLabel: cancelLabel,
189
+ disabled: saving,
190
+ label: cancelLabel,
191
+ testID: cancelTestID,
192
+ variant: "outline",
193
+ onPress: onCancel
194
+ }
195
+ ) : null,
196
+ /* @__PURE__ */ jsxRuntime.jsx(
197
+ uiButtons.Button,
198
+ {
199
+ accessibilityHint: saveHint ?? saveLabel,
200
+ accessibilityLabel: saveLabel,
201
+ disabled: saveDisabled,
202
+ label: saveLabel,
203
+ loading: saving,
204
+ testID: saveTestID,
205
+ variant: "primary",
206
+ onPress: onSave
207
+ }
208
+ )
209
+ ] });
80
210
  var IS_WEB = reactNative.Platform.OS === "web";
81
211
  var TRANSITION_PROPERTY = "border-color, background-color, box-shadow";
82
212
  var TRANSITION_DURATION = "0.14s";
@@ -109,34 +239,35 @@ function useThemedInput(options = {}) {
109
239
  const { hasError = false, onFocus, onBlur } = options;
110
240
  const { theme } = uiFeedback.useUi();
111
241
  const { colors, palette, semantic } = theme;
112
- const [isFocused, setIsFocused] = React3.useState(false);
113
- const [isHovered, setIsHovered] = React3.useState(false);
114
- const handleFocus = React3.useCallback(
242
+ const [isFocused, setIsFocused] = React.useState(false);
243
+ const [isHovered, setIsHovered] = React.useState(false);
244
+ const handleFocus = React.useCallback(
115
245
  (e) => {
116
246
  setIsFocused(true);
117
247
  onFocus?.(e);
118
248
  },
119
249
  [onFocus]
120
250
  );
121
- const handleBlur = React3.useCallback(
251
+ const handleBlur = React.useCallback(
122
252
  (e) => {
123
253
  setIsFocused(false);
124
254
  onBlur?.(e);
125
255
  },
126
256
  [onBlur]
127
257
  );
128
- const handleHoverIn = React3.useCallback(() => setIsHovered(true), []);
129
- const handleHoverOut = React3.useCallback(() => setIsHovered(false), []);
258
+ const handleHoverIn = React.useCallback(() => setIsHovered(true), []);
259
+ const handleHoverOut = React.useCallback(() => setIsHovered(false), []);
130
260
  const primary = palette.primary["500"];
131
261
  const errorColor = semantic.error["500"];
132
- const style = React3.useMemo(() => {
262
+ const style = React.useMemo(() => {
133
263
  const restBorder = hasError ? errorColor : colors.border;
134
264
  const focusBorder = hasError ? errorColor : primary;
135
265
  const borderColor = isFocused ? focusBorder : isHovered ? withAlpha(colors.text, HOVER_BORDER_ALPHA) : restBorder;
136
266
  const backgroundColor = isFocused ? colors.surface : colors.surfaceElevated;
137
267
  const base = { color: colors.text, borderColor, backgroundColor };
138
268
  if (!IS_WEB) return base;
139
- const ring = isFocused && !hasError ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(primary, FOCUS_RING_ALPHA)}` : "none";
269
+ const ringColor = hasError ? errorColor : primary;
270
+ const ring = isFocused ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(ringColor, FOCUS_RING_ALPHA)}` : "none";
140
271
  const webStyle = {
141
272
  transitionProperty: TRANSITION_PROPERTY,
142
273
  // Collapse the eased transition to instant when the user prefers reduced motion.
@@ -157,8 +288,8 @@ function useThemedInput(options = {}) {
157
288
  primary,
158
289
  errorColor
159
290
  ]);
160
- const focusBind = React3.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
161
- const hoverBind = React3.useMemo(
291
+ const focusBind = React.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
292
+ const hoverBind = React.useMemo(
162
293
  () => IS_WEB ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
163
294
  [handleHoverIn, handleHoverOut]
164
295
  );
@@ -172,7 +303,7 @@ function useThemedInput(options = {}) {
172
303
  };
173
304
  }
174
305
  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) {
306
+ var ThemedTextInput = React__default.default.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
176
307
  const {
177
308
  style: themedStyle,
178
309
  placeholderTextColor: themedPlaceholder,
@@ -202,7 +333,7 @@ var INPUT_PADDING_V = 10;
202
333
  var INPUT_PADDING_H = 12;
203
334
  var INPUT_BORDER_RADIUS = 8;
204
335
  var INPUT_BORDER_WIDTH = 1;
205
- var styles2 = reactNative.StyleSheet.create({
336
+ var styles3 = reactNative.StyleSheet.create({
206
337
  input: {
207
338
  paddingVertical: INPUT_PADDING_V,
208
339
  paddingHorizontal: INPUT_PADDING_H,
@@ -224,7 +355,7 @@ var FormField = ({
224
355
  describedById,
225
356
  hasError,
226
357
  requiredField: required,
227
- style: styles2.input,
358
+ style: styles3.input,
228
359
  testID: "form-field-input",
229
360
  ...textInputProps
230
361
  }
@@ -244,6 +375,14 @@ var RGB_R_END = 2;
244
375
  var RGB_G_END = 4;
245
376
  var RGB_B_END = 6;
246
377
  var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
378
+ var CHIP_GUTTER = 8;
379
+ var DEFAULT_CHIP_TESTID_PREFIX = "chip-selector-chip";
380
+ var ON_BRAND_SCALE = "onBrand";
381
+ var MAIN_STEP = "500";
382
+ function onBrandInk(palette) {
383
+ const scales = palette;
384
+ return scales[ON_BRAND_SCALE]?.[MAIN_STEP] ?? WHITE_COLOR;
385
+ }
247
386
  function withAlpha2(hex, alpha) {
248
387
  const normalized = hex.replace("#", "");
249
388
  const isShort = normalized.length === HEX_SHORT_LENGTH2;
@@ -254,18 +393,18 @@ function withAlpha2(hex, alpha) {
254
393
  const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
255
394
  return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
256
395
  }
257
- var styles3 = reactNative.StyleSheet.create({
258
- label: {
259
- marginBottom: 8,
260
- fontWeight: "600"
261
- },
396
+ var styles4 = reactNative.StyleSheet.create({
262
397
  chipContainer: {
263
398
  flexDirection: "row",
264
- flexWrap: "wrap"
399
+ flexWrap: "wrap",
400
+ // Each chip's bottom gutter also hangs off the LAST row, stacking on `Field`'s container
401
+ // margin (8 + 16 = 24) and dropping the block below the `FormField` beside it. Cancelling it
402
+ // is standard gutter compensation: inner rows keep their 8px, the block ends at Field's 16.
403
+ marginBottom: -CHIP_GUTTER
265
404
  },
266
405
  chipWrapper: {
267
- marginRight: 8,
268
- marginBottom: 8
406
+ marginRight: CHIP_GUTTER,
407
+ marginBottom: CHIP_GUTTER
269
408
  },
270
409
  chip: {
271
410
  paddingHorizontal: 12,
@@ -283,14 +422,26 @@ var styles3 = reactNative.StyleSheet.create({
283
422
  chipText: {},
284
423
  chipTextOutline: { fontSize: OUTLINE_FONT_SIZE }
285
424
  });
425
+ function chipIdentity(option, selected, config) {
426
+ return {
427
+ testID: `${config.prefix}-${String(option.value)}`,
428
+ // The English default is retained verbatim: erevna/katalogos assert the exact string
429
+ // `Selects Red`. Consumers that localize pass `optionAccessibilityHint`.
430
+ accessibilityHint: config.hint ?? `Selects ${option.label}`,
431
+ ariaPressed: config.multiple ? selected : void 0
432
+ };
433
+ }
286
434
  function OutlineChip({
287
435
  option,
288
436
  selected,
289
437
  disabled,
290
438
  colors,
439
+ testID,
440
+ accessibilityHint,
441
+ ariaPressed,
291
442
  onPress
292
443
  }) {
293
- const [hovered, setHovered] = React3__default.default.useState(false);
444
+ const [hovered, setHovered] = React__default.default.useState(false);
294
445
  const active = selected || hovered;
295
446
  const borderColor = active ? colors.primary : colors.border;
296
447
  const backgroundColor = selected ? colors.tint : colors.muted;
@@ -298,18 +449,19 @@ function OutlineChip({
298
449
  return /* @__PURE__ */ jsxRuntime.jsx(
299
450
  reactNative.Pressable,
300
451
  {
301
- accessibilityHint: `Selects ${option.label}`,
452
+ accessibilityHint,
302
453
  accessibilityLabel: option.label,
303
454
  accessibilityRole: "button",
304
455
  accessibilityState: { selected, disabled },
456
+ "aria-pressed": ariaPressed,
305
457
  disabled,
306
458
  hitSlop: CHIP_HIT_SLOP,
307
- style: styles3.chipWrapper,
308
- testID: `chip-selector-chip-${String(option.value)}`,
459
+ style: styles4.chipWrapper,
460
+ testID,
309
461
  onHoverIn: () => setHovered(true),
310
462
  onHoverOut: () => setHovered(false),
311
463
  onPress,
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 }) })
464
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles4.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.chipText, styles4.chipTextOutline, { color: textColor }], children: option.label }) })
313
465
  }
314
466
  );
315
467
  }
@@ -318,26 +470,35 @@ function SolidChip({
318
470
  selected,
319
471
  disabled,
320
472
  colors,
473
+ testID,
474
+ accessibilityHint,
475
+ ariaPressed,
321
476
  onPress
322
477
  }) {
323
478
  const backgroundColor = selected ? colors.primary : TRANSPARENT_COLOR;
324
- const textColor = selected ? WHITE_COLOR : colors.textStrong;
479
+ const textColor = selected ? colors.onPrimary : colors.textStrong;
325
480
  return /* @__PURE__ */ jsxRuntime.jsx(
326
481
  reactNative.TouchableOpacity,
327
482
  {
328
- accessibilityHint: `Selects ${option.label}`,
483
+ accessibilityHint,
329
484
  accessibilityLabel: option.label,
330
485
  accessibilityRole: "button",
331
486
  accessibilityState: { selected, disabled },
487
+ "aria-pressed": ariaPressed,
332
488
  disabled,
333
489
  hitSlop: CHIP_HIT_SLOP,
334
- style: styles3.chipWrapper,
335
- testID: `chip-selector-chip-${String(option.value)}`,
490
+ style: styles4.chipWrapper,
491
+ testID,
336
492
  onPress,
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 }) })
493
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles4.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.chipText, { color: textColor }], children: option.label }) })
338
494
  }
339
495
  );
340
496
  }
497
+ var IS_WEB3 = reactNative.Platform.OS === "web";
498
+ function groupA11yProps(describedById, hasError) {
499
+ if (!IS_WEB3) return {};
500
+ return { "aria-invalid": hasError ? true : void 0, "aria-describedby": describedById };
501
+ }
341
502
  var ChipSelector = ({
342
503
  label,
343
504
  options,
@@ -346,59 +507,53 @@ var ChipSelector = ({
346
507
  multiple = false,
347
508
  disabled = false,
348
509
  variant = "solid",
349
- containerStyle
510
+ containerStyle,
511
+ required = false,
512
+ error,
513
+ testID,
514
+ chipTestIDPrefix = DEFAULT_CHIP_TESTID_PREFIX,
515
+ optionAccessibilityHint
350
516
  }) => {
351
517
  const { theme } = uiFeedback.useUi();
352
518
  const { colors, palette } = theme;
353
- const hasLabel = typeof label === "string" && label !== "";
354
- const chipColors = React3__default.default.useMemo(() => {
519
+ const chipColors = React__default.default.useMemo(() => {
355
520
  const primary = palette.primary["500"];
356
521
  return {
357
522
  border: colors.border,
358
523
  textStrong: colors.text,
359
524
  textMuted: colors.textSecondary,
360
525
  primary,
526
+ onPrimary: onBrandInk(palette),
361
527
  muted: colors.background,
362
528
  tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
363
529
  };
364
- }, [colors.border, colors.text, colors.textSecondary, colors.background, palette.primary]);
365
- const labelStyle = React3__default.default.useMemo(() => ({ color: colors.text }), [colors.text]);
530
+ }, [colors.border, colors.text, colors.textSecondary, colors.background, palette]);
366
531
  function isSelected(optionValue) {
367
532
  if (multiple && Array.isArray(value))
368
533
  return value.includes(optionValue);
369
534
  return value === optionValue;
370
535
  }
371
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: containerStyle, children: [
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) => {
374
- const selected = isSelected(option.value);
375
- const onPress = () => onChange(option.value);
376
- return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(
377
- OutlineChip,
378
- {
379
- colors: chipColors,
380
- disabled,
381
- option,
382
- selected,
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
- ] });
536
+ function renderChip(option) {
537
+ const selected = isSelected(option.value);
538
+ const chipProps = {
539
+ colors: chipColors,
540
+ disabled,
541
+ option,
542
+ selected,
543
+ ...chipIdentity(option, selected, {
544
+ prefix: chipTestIDPrefix,
545
+ hint: optionAccessibilityHint,
546
+ multiple
547
+ }),
548
+ onPress: () => onChange(option.value)
549
+ };
550
+ const key = String(option.value);
551
+ return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(OutlineChip, { ...chipProps }, key) : /* @__PURE__ */ jsxRuntime.jsx(SolidChip, { ...chipProps }, key);
552
+ }
553
+ return /* @__PURE__ */ jsxRuntime.jsx(Field, { containerStyle, error, label, required, testID, children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles4.chipContainer, ...groupA11yProps(describedById, hasError), children: options.map(renderChip) }) });
399
554
  };
400
555
  var DESCRIPTION_FONT_SIZE = 12;
401
- var styles4 = reactNative.StyleSheet.create({
556
+ var styles5 = reactNative.StyleSheet.create({
402
557
  container: {
403
558
  flexDirection: "row",
404
559
  justifyContent: "space-between",
@@ -429,7 +584,7 @@ var FormSwitch = ({
429
584
  const { theme } = uiFeedback.useUi();
430
585
  const { colors } = theme;
431
586
  const hasDescription = typeof description === "string" && description !== "";
432
- const themeStyles = React3__default.default.useMemo(() => {
587
+ const themeStyles = React__default.default.useMemo(() => {
433
588
  const labelStyle = { color: colors.text };
434
589
  const descriptionStyle = { color: colors.textSecondary };
435
590
  return { label: labelStyle, description: descriptionStyle };
@@ -438,13 +593,13 @@ var FormSwitch = ({
438
593
  reactNative.View,
439
594
  {
440
595
  style: [
441
- styles4.container,
596
+ styles5.container,
442
597
  containerStyle
443
598
  ],
444
599
  children: [
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
600
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles5.textColumn, children: [
601
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.label, themeStyles.label], children: label }),
602
+ hasDescription ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles5.description, themeStyles.description], children: description }) : null
448
603
  ] }),
449
604
  /* @__PURE__ */ jsxRuntime.jsx(
450
605
  reactNative.Switch,
@@ -464,12 +619,12 @@ var FormSwitch = ({
464
619
  );
465
620
  };
466
621
  var ACTIVATION_KEYS = ["Enter", " ", "Spacebar"];
467
- var HINT_FONT_SIZE = 12;
622
+ var HINT_FONT_SIZE2 = 12;
468
623
  var BOX_SIZE = 20;
469
624
  var BOX_RADIUS = 5;
470
625
  var CHECK_FONT_SIZE = 14;
471
626
  var DISABLED_OPACITY = 0.5;
472
- var styles5 = reactNative.StyleSheet.create({
627
+ var styles6 = reactNative.StyleSheet.create({
473
628
  container: {
474
629
  flexDirection: "row",
475
630
  alignItems: "flex-start",
@@ -498,7 +653,7 @@ var styles5 = reactNative.StyleSheet.create({
498
653
  fontWeight: "600"
499
654
  },
500
655
  hint: {
501
- fontSize: HINT_FONT_SIZE,
656
+ fontSize: HINT_FONT_SIZE2,
502
657
  marginTop: 2
503
658
  }
504
659
  });
@@ -520,7 +675,7 @@ var FormCheckbox = ({
520
675
  const brand = palette.primary["500"];
521
676
  const hintText = typeof hint === "string" && hint !== "" ? hint : description;
522
677
  const hasHint = typeof hintText === "string" && hintText !== "";
523
- const themeStyles = React3__default.default.useMemo(() => {
678
+ const themeStyles = React__default.default.useMemo(() => {
524
679
  const boxOn = { backgroundColor: brand, borderColor: brand };
525
680
  const boxOff = { backgroundColor: "transparent", borderColor: colors.border };
526
681
  const labelStyle = { color: colors.text };
@@ -551,14 +706,14 @@ var FormCheckbox = ({
551
706
  disabled,
552
707
  onPress: toggle,
553
708
  role: "checkbox",
554
- style: [styles5.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
709
+ style: [styles6.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
555
710
  testID,
556
711
  ...keyboardProps,
557
712
  children: [
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
713
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles6.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
714
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles6.textColumn, children: [
715
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.label, themeStyles.label], children: label }),
716
+ hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.hint, themeStyles.hint], children: hintText }) : null
562
717
  ] })
563
718
  ]
564
719
  }
@@ -566,7 +721,9 @@ var FormCheckbox = ({
566
721
  };
567
722
 
568
723
  exports.ChipSelector = ChipSelector;
724
+ exports.DEFAULT_OPTIONAL_LABEL = DEFAULT_OPTIONAL_LABEL;
569
725
  exports.Field = Field;
726
+ exports.FormActions = FormActions;
570
727
  exports.FormCheckbox = FormCheckbox;
571
728
  exports.FormField = FormField;
572
729
  exports.FormSwitch = FormSwitch;