@dloizides/ui-forms 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,9 +1,77 @@
1
- import React3, { useState, useCallback, useMemo } from 'react';
2
- import { Platform, TextInput, StyleSheet, View, Text, Switch, Pressable, TouchableOpacity } from 'react-native';
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
+ import { StyleSheet, Platform, TextInput, View, Text, Switch, Pressable, TouchableOpacity } from 'react-native';
3
3
  import { useUi } from '@dloizides/ui-feedback';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
5
5
 
6
- // src/FormField/FormField.tsx
6
+ // src/Field/Field.tsx
7
+ var CONTAINER_MARGIN_BOTTOM = 16;
8
+ var LABEL_FONT_SIZE = 13;
9
+ var LABEL_FONT_WEIGHT = "600";
10
+ var LABEL_MARGIN_BOTTOM = 4;
11
+ var ERROR_FONT_SIZE = 12;
12
+ var ERROR_MARGIN_TOP = 4;
13
+ var styles = StyleSheet.create({
14
+ container: {
15
+ marginBottom: CONTAINER_MARGIN_BOTTOM
16
+ },
17
+ label: {
18
+ marginBottom: LABEL_MARGIN_BOTTOM,
19
+ fontSize: LABEL_FONT_SIZE,
20
+ fontWeight: LABEL_FONT_WEIGHT
21
+ },
22
+ errorText: {
23
+ fontSize: ERROR_FONT_SIZE,
24
+ marginTop: ERROR_MARGIN_TOP
25
+ }
26
+ });
27
+ var fieldSeq = 0;
28
+ function useFieldErrorId() {
29
+ const [id] = React.useState(() => {
30
+ fieldSeq += 1;
31
+ return `field-error-${String(fieldSeq)}`;
32
+ });
33
+ return id;
34
+ }
35
+ function hasFieldError(error) {
36
+ return typeof error === "string" && error !== "";
37
+ }
38
+ var Field = ({
39
+ label,
40
+ children,
41
+ required = false,
42
+ error,
43
+ containerStyle,
44
+ testID
45
+ }) => {
46
+ const { theme } = useUi();
47
+ const { colors, semantic } = theme;
48
+ const errorColor = semantic.error["500"];
49
+ const hasError = hasFieldError(error);
50
+ const hasLabel = typeof label === "string" && label !== "";
51
+ const errorId = useFieldErrorId();
52
+ const themeStyles = React.useMemo(
53
+ () => ({
54
+ label: { color: colors.textSecondary },
55
+ requiredMark: { color: errorColor },
56
+ errorText: { color: errorColor }
57
+ }),
58
+ [colors.textSecondary, errorColor]
59
+ );
60
+ const control = typeof children === "function" ? children({ describedById: hasError ? errorId : void 0, hasError }) : children;
61
+ return /* @__PURE__ */ jsxs(View, { style: [styles.container, containerStyle], testID, children: [
62
+ hasLabel ? /* @__PURE__ */ jsxs(Text, { style: [styles.label, themeStyles.label], children: [
63
+ label,
64
+ " ",
65
+ required ? (
66
+ // The asterisk is decorative — the control's `aria-required` conveys "required" to
67
+ // assistive tech, so a screen reader says "required" instead of "star".
68
+ /* @__PURE__ */ jsx(Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: themeStyles.requiredMark, children: "*" })
69
+ ) : null
70
+ ] }) : null,
71
+ control,
72
+ hasError ? /* @__PURE__ */ jsx(Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
73
+ ] });
74
+ };
7
75
  var IS_WEB = Platform.OS === "web";
8
76
  var TRANSITION_PROPERTY = "border-color, background-color, box-shadow";
9
77
  var TRANSITION_DURATION = "0.14s";
@@ -99,7 +167,7 @@ function useThemedInput(options = {}) {
99
167
  };
100
168
  }
101
169
  var IS_WEB2 = Platform.OS === "web";
102
- var ThemedTextInput = React3.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
170
+ var ThemedTextInput = React.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
103
171
  const {
104
172
  style: themedStyle,
105
173
  placeholderTextColor: themedPlaceholder,
@@ -125,85 +193,37 @@ var ThemedTextInput = React3.forwardRef(function ThemedTextInput2({ hasError = f
125
193
  }
126
194
  );
127
195
  });
128
- var CONTAINER_MARGIN_BOTTOM = 16;
129
196
  var INPUT_PADDING_V = 10;
130
197
  var INPUT_PADDING_H = 12;
131
198
  var INPUT_BORDER_RADIUS = 8;
132
- var ERROR_FONT_SIZE = 12;
133
- var LABEL_FONT_SIZE = 13;
134
- var styles = StyleSheet.create({
135
- container: {
136
- marginBottom: CONTAINER_MARGIN_BOTTOM
137
- },
138
- label: {
139
- marginBottom: 4,
140
- fontSize: LABEL_FONT_SIZE,
141
- fontWeight: "600"
142
- },
199
+ var INPUT_BORDER_WIDTH = 1;
200
+ var styles2 = StyleSheet.create({
143
201
  input: {
144
202
  paddingVertical: INPUT_PADDING_V,
145
203
  paddingHorizontal: INPUT_PADDING_H,
146
204
  borderRadius: INPUT_BORDER_RADIUS,
147
- borderWidth: 1
148
- },
149
- errorText: {
150
- fontSize: ERROR_FONT_SIZE,
151
- marginTop: 4
205
+ borderWidth: INPUT_BORDER_WIDTH
152
206
  }
153
207
  });
154
- var formFieldSeq = 0;
155
- function useErrorId() {
156
- const [id] = React3.useState(() => {
157
- formFieldSeq += 1;
158
- return `form-field-error-${String(formFieldSeq)}`;
159
- });
160
- return id;
161
- }
162
208
  var FormField = ({
163
209
  label,
164
210
  required = false,
165
211
  error,
166
212
  containerStyle,
167
213
  ...textInputProps
168
- }) => {
169
- const { theme } = useUi();
170
- const { colors, semantic } = theme;
171
- const errorColor = semantic.error["500"];
172
- const hasError = typeof error === "string" && error !== "";
173
- const errorId = useErrorId();
174
- const themeStyles = React3.useMemo(
175
- () => ({
176
- label: { color: colors.textSecondary },
177
- requiredMark: { color: errorColor },
178
- errorText: { color: errorColor }
179
- }),
180
- [colors.textSecondary, errorColor]
181
- );
182
- return /* @__PURE__ */ jsxs(View, { style: [styles.container, containerStyle], children: [
183
- /* @__PURE__ */ jsxs(Text, { style: [styles.label, themeStyles.label], children: [
184
- label,
185
- " ",
186
- required ? (
187
- // The asterisk is decorative — `requiredField` conveys "required" to assistive tech.
188
- /* @__PURE__ */ jsx(Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: themeStyles.requiredMark, children: "*" })
189
- ) : null
190
- ] }),
191
- /* @__PURE__ */ jsx(
192
- ThemedTextInput,
193
- {
194
- accessibilityHint: `Enter ${label}`,
195
- accessibilityLabel: label,
196
- describedById: hasError ? errorId : void 0,
197
- hasError,
198
- requiredField: required,
199
- style: styles.input,
200
- testID: "form-field-input",
201
- ...textInputProps
202
- }
203
- ),
204
- hasError ? /* @__PURE__ */ jsx(Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
205
- ] });
206
- };
214
+ }) => /* @__PURE__ */ jsx(Field, { containerStyle, error, label, required, children: ({ describedById, hasError }) => /* @__PURE__ */ jsx(
215
+ ThemedTextInput,
216
+ {
217
+ accessibilityHint: `Enter ${label}`,
218
+ accessibilityLabel: label,
219
+ describedById,
220
+ hasError,
221
+ requiredField: required,
222
+ style: styles2.input,
223
+ testID: "form-field-input",
224
+ ...textInputProps
225
+ }
226
+ ) });
207
227
  var TRANSPARENT_COLOR = "transparent";
208
228
  var WHITE_COLOR = "#fff";
209
229
  var SOLID_RADIUS = 20;
@@ -219,6 +239,7 @@ var RGB_R_END = 2;
219
239
  var RGB_G_END = 4;
220
240
  var RGB_B_END = 6;
221
241
  var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
242
+ var CHIP_GUTTER = 8;
222
243
  function withAlpha2(hex, alpha) {
223
244
  const normalized = hex.replace("#", "");
224
245
  const isShort = normalized.length === HEX_SHORT_LENGTH2;
@@ -229,18 +250,18 @@ function withAlpha2(hex, alpha) {
229
250
  const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
230
251
  return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
231
252
  }
232
- var styles2 = StyleSheet.create({
233
- label: {
234
- marginBottom: 8,
235
- fontWeight: "600"
236
- },
253
+ var styles3 = StyleSheet.create({
237
254
  chipContainer: {
238
255
  flexDirection: "row",
239
- flexWrap: "wrap"
256
+ flexWrap: "wrap",
257
+ // Each chip's bottom gutter also hangs off the LAST row, stacking on `Field`'s container
258
+ // margin (8 + 16 = 24) and dropping the block below the `FormField` beside it. Cancelling it
259
+ // is standard gutter compensation: inner rows keep their 8px, the block ends at Field's 16.
260
+ marginBottom: -CHIP_GUTTER
240
261
  },
241
262
  chipWrapper: {
242
- marginRight: 8,
243
- marginBottom: 8
263
+ marginRight: CHIP_GUTTER,
264
+ marginBottom: CHIP_GUTTER
244
265
  },
245
266
  chip: {
246
267
  paddingHorizontal: 12,
@@ -265,7 +286,7 @@ function OutlineChip({
265
286
  colors,
266
287
  onPress
267
288
  }) {
268
- const [hovered, setHovered] = React3.useState(false);
289
+ const [hovered, setHovered] = React.useState(false);
269
290
  const active = selected || hovered;
270
291
  const borderColor = active ? colors.primary : colors.border;
271
292
  const backgroundColor = selected ? colors.tint : colors.muted;
@@ -279,12 +300,12 @@ function OutlineChip({
279
300
  accessibilityState: { selected, disabled },
280
301
  disabled,
281
302
  hitSlop: CHIP_HIT_SLOP,
282
- style: styles2.chipWrapper,
303
+ style: styles3.chipWrapper,
283
304
  testID: `chip-selector-chip-${String(option.value)}`,
284
305
  onHoverIn: () => setHovered(true),
285
306
  onHoverOut: () => setHovered(false),
286
307
  onPress,
287
- children: /* @__PURE__ */ jsx(View, { style: [styles2.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles2.chipText, styles2.chipTextOutline, { color: textColor }], children: option.label }) })
308
+ children: /* @__PURE__ */ jsx(View, { style: [styles3.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles3.chipText, styles3.chipTextOutline, { color: textColor }], children: option.label }) })
288
309
  }
289
310
  );
290
311
  }
@@ -306,13 +327,18 @@ function SolidChip({
306
327
  accessibilityState: { selected, disabled },
307
328
  disabled,
308
329
  hitSlop: CHIP_HIT_SLOP,
309
- style: styles2.chipWrapper,
330
+ style: styles3.chipWrapper,
310
331
  testID: `chip-selector-chip-${String(option.value)}`,
311
332
  onPress,
312
- children: /* @__PURE__ */ jsx(View, { style: [styles2.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles2.chipText, { color: textColor }], children: option.label }) })
333
+ children: /* @__PURE__ */ jsx(View, { style: [styles3.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles3.chipText, { color: textColor }], children: option.label }) })
313
334
  }
314
335
  );
315
336
  }
337
+ var IS_WEB3 = Platform.OS === "web";
338
+ function groupA11yProps(describedById, hasError) {
339
+ if (!IS_WEB3) return {};
340
+ return { "aria-invalid": hasError ? true : void 0, "aria-describedby": describedById };
341
+ }
316
342
  var ChipSelector = ({
317
343
  label,
318
344
  options,
@@ -321,12 +347,14 @@ var ChipSelector = ({
321
347
  multiple = false,
322
348
  disabled = false,
323
349
  variant = "solid",
324
- containerStyle
350
+ containerStyle,
351
+ required = false,
352
+ error,
353
+ testID
325
354
  }) => {
326
355
  const { theme } = useUi();
327
356
  const { colors, palette } = theme;
328
- const hasLabel = typeof label === "string" && label !== "";
329
- const chipColors = React3.useMemo(() => {
357
+ const chipColors = React.useMemo(() => {
330
358
  const primary = palette.primary["500"];
331
359
  return {
332
360
  border: colors.border,
@@ -337,43 +365,26 @@ var ChipSelector = ({
337
365
  tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
338
366
  };
339
367
  }, [colors.border, colors.text, colors.textSecondary, colors.background, palette.primary]);
340
- const labelStyle = React3.useMemo(() => ({ color: colors.text }), [colors.text]);
341
368
  function isSelected(optionValue) {
342
369
  if (multiple && Array.isArray(value))
343
370
  return value.includes(optionValue);
344
371
  return value === optionValue;
345
372
  }
346
- return /* @__PURE__ */ jsxs(View, { style: containerStyle, children: [
347
- hasLabel ? /* @__PURE__ */ jsx(Text, { style: [styles2.label, labelStyle], children: label }) : null,
348
- /* @__PURE__ */ jsx(View, { style: styles2.chipContainer, children: options.map((option) => {
349
- const selected = isSelected(option.value);
350
- const onPress = () => onChange(option.value);
351
- return variant === "outline" ? /* @__PURE__ */ jsx(
352
- OutlineChip,
353
- {
354
- colors: chipColors,
355
- disabled,
356
- option,
357
- selected,
358
- onPress
359
- },
360
- String(option.value)
361
- ) : /* @__PURE__ */ jsx(
362
- SolidChip,
363
- {
364
- colors: chipColors,
365
- disabled,
366
- option,
367
- selected,
368
- onPress
369
- },
370
- String(option.value)
371
- );
372
- }) })
373
- ] });
373
+ function renderChip(option) {
374
+ const chipProps = {
375
+ colors: chipColors,
376
+ disabled,
377
+ option,
378
+ selected: isSelected(option.value),
379
+ onPress: () => onChange(option.value)
380
+ };
381
+ const key = String(option.value);
382
+ return variant === "outline" ? /* @__PURE__ */ jsx(OutlineChip, { ...chipProps }, key) : /* @__PURE__ */ jsx(SolidChip, { ...chipProps }, key);
383
+ }
384
+ return /* @__PURE__ */ jsx(Field, { containerStyle, error, label, required, testID, children: ({ describedById, hasError }) => /* @__PURE__ */ jsx(View, { style: styles3.chipContainer, ...groupA11yProps(describedById, hasError), children: options.map(renderChip) }) });
374
385
  };
375
386
  var DESCRIPTION_FONT_SIZE = 12;
376
- var styles3 = StyleSheet.create({
387
+ var styles4 = StyleSheet.create({
377
388
  container: {
378
389
  flexDirection: "row",
379
390
  justifyContent: "space-between",
@@ -404,7 +415,7 @@ var FormSwitch = ({
404
415
  const { theme } = useUi();
405
416
  const { colors } = theme;
406
417
  const hasDescription = typeof description === "string" && description !== "";
407
- const themeStyles = React3.useMemo(() => {
418
+ const themeStyles = React.useMemo(() => {
408
419
  const labelStyle = { color: colors.text };
409
420
  const descriptionStyle = { color: colors.textSecondary };
410
421
  return { label: labelStyle, description: descriptionStyle };
@@ -413,13 +424,13 @@ var FormSwitch = ({
413
424
  View,
414
425
  {
415
426
  style: [
416
- styles3.container,
427
+ styles4.container,
417
428
  containerStyle
418
429
  ],
419
430
  children: [
420
- /* @__PURE__ */ jsxs(View, { style: styles3.textColumn, children: [
421
- /* @__PURE__ */ jsx(Text, { style: [styles3.label, themeStyles.label], children: label }),
422
- hasDescription ? /* @__PURE__ */ jsx(Text, { style: [styles3.description, themeStyles.description], children: description }) : null
431
+ /* @__PURE__ */ jsxs(View, { style: styles4.textColumn, children: [
432
+ /* @__PURE__ */ jsx(Text, { style: [styles4.label, themeStyles.label], children: label }),
433
+ hasDescription ? /* @__PURE__ */ jsx(Text, { style: [styles4.description, themeStyles.description], children: description }) : null
423
434
  ] }),
424
435
  /* @__PURE__ */ jsx(
425
436
  Switch,
@@ -444,7 +455,7 @@ var BOX_SIZE = 20;
444
455
  var BOX_RADIUS = 5;
445
456
  var CHECK_FONT_SIZE = 14;
446
457
  var DISABLED_OPACITY = 0.5;
447
- var styles4 = StyleSheet.create({
458
+ var styles5 = StyleSheet.create({
448
459
  container: {
449
460
  flexDirection: "row",
450
461
  alignItems: "flex-start",
@@ -495,7 +506,7 @@ var FormCheckbox = ({
495
506
  const brand = palette.primary["500"];
496
507
  const hintText = typeof hint === "string" && hint !== "" ? hint : description;
497
508
  const hasHint = typeof hintText === "string" && hintText !== "";
498
- const themeStyles = React3.useMemo(() => {
509
+ const themeStyles = React.useMemo(() => {
499
510
  const boxOn = { backgroundColor: brand, borderColor: brand };
500
511
  const boxOff = { backgroundColor: "transparent", borderColor: colors.border };
501
512
  const labelStyle = { color: colors.text };
@@ -526,20 +537,20 @@ var FormCheckbox = ({
526
537
  disabled,
527
538
  onPress: toggle,
528
539
  role: "checkbox",
529
- style: [styles4.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
540
+ style: [styles5.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
530
541
  testID,
531
542
  ...keyboardProps,
532
543
  children: [
533
- /* @__PURE__ */ jsx(View, { style: [styles4.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsx(Text, { style: [styles4.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
534
- /* @__PURE__ */ jsxs(View, { style: styles4.textColumn, children: [
535
- /* @__PURE__ */ jsx(Text, { style: [styles4.label, themeStyles.label], children: label }),
536
- hasHint ? /* @__PURE__ */ jsx(Text, { style: [styles4.hint, themeStyles.hint], children: hintText }) : null
544
+ /* @__PURE__ */ jsx(View, { style: [styles5.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsx(Text, { style: [styles5.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
545
+ /* @__PURE__ */ jsxs(View, { style: styles5.textColumn, children: [
546
+ /* @__PURE__ */ jsx(Text, { style: [styles5.label, themeStyles.label], children: label }),
547
+ hasHint ? /* @__PURE__ */ jsx(Text, { style: [styles5.hint, themeStyles.hint], children: hintText }) : null
537
548
  ] })
538
549
  ]
539
550
  }
540
551
  );
541
552
  };
542
553
 
543
- export { ChipSelector, FormCheckbox, FormField, FormSwitch, ThemedTextInput, useThemedInput };
554
+ export { ChipSelector, Field, FormCheckbox, FormField, FormSwitch, ThemedTextInput, useThemedInput };
544
555
  //# sourceMappingURL=index.mjs.map
545
556
  //# sourceMappingURL=index.mjs.map