@dloizides/ui-forms 1.8.0 → 1.9.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
@@ -42,6 +42,40 @@ function resolveFieldMarker(required, optional) {
42
42
  if (optional) return "optional";
43
43
  return "none";
44
44
  }
45
+ var FIELD_STACK_MARGIN_BOTTOM = 16;
46
+ var FIELD_STACK_LABEL_MARGIN_BOTTOM = 4;
47
+ var FIELD_STACK_CHILD_MARGIN_TOP = 4;
48
+ var FIELD_CONTRACT_GAP = 6;
49
+ var fieldSpacingStyles = reactNative.StyleSheet.create({
50
+ stackContainer: { marginBottom: FIELD_STACK_MARGIN_BOTTOM },
51
+ stackLabel: { marginBottom: FIELD_STACK_LABEL_MARGIN_BOTTOM },
52
+ stackChild: { marginTop: FIELD_STACK_CHILD_MARGIN_TOP },
53
+ /**
54
+ * The gap model sets NO margins — not even zeroed ones. An explicit `marginBottom: 0` here would
55
+ * defeat a caller who legitimately wants to push one field down via `containerStyle`, because
56
+ * `containerStyle` composes AFTER this and a zero is indistinguishable from "unset" to a reader.
57
+ */
58
+ gapContainer: { gap: FIELD_CONTRACT_GAP },
59
+ none: {}
60
+ });
61
+ function resolveFieldSpacing(explicit, insideGrid) {
62
+ if (explicit !== void 0) return explicit;
63
+ return insideGrid ? "gap" : "stack";
64
+ }
65
+ function fieldSpacingStyleSet(spacing) {
66
+ if (spacing === "gap") {
67
+ return {
68
+ container: fieldSpacingStyles.gapContainer,
69
+ label: fieldSpacingStyles.none,
70
+ child: fieldSpacingStyles.none
71
+ };
72
+ }
73
+ return {
74
+ container: fieldSpacingStyles.stackContainer,
75
+ label: fieldSpacingStyles.stackLabel,
76
+ child: fieldSpacingStyles.stackChild
77
+ };
78
+ }
45
79
  var FIELD_FONT_SIZE = 13;
46
80
  var FIELD_FONT_WEIGHT = "600";
47
81
  var CONTROL_FONT_SIZE = 11;
@@ -62,27 +96,25 @@ var labelVariantStyles = reactNative.StyleSheet.create({
62
96
  function resolveLabelVariantStyle(variant) {
63
97
  return variant === "control" ? labelVariantStyles.control : labelVariantStyles.field;
64
98
  }
65
- var CONTAINER_MARGIN_BOTTOM = 16;
66
- var LABEL_MARGIN_BOTTOM = 4;
99
+ var FormGridContext = React__default.default.createContext(null);
100
+ var GapOwnedContext = React__default.default.createContext(false);
101
+ var FormGridProvider = FormGridContext.Provider;
102
+ var GapOwnedProvider = GapOwnedContext.Provider;
103
+ function useFormGridMetrics() {
104
+ return React__default.default.useContext(FormGridContext);
105
+ }
106
+ function useIsGapOwned() {
107
+ return React__default.default.useContext(GapOwnedContext);
108
+ }
67
109
  var ERROR_FONT_SIZE = 12;
68
- var ERROR_MARGIN_TOP = 4;
69
110
  var HINT_FONT_SIZE = 12;
70
- var HINT_MARGIN_TOP = 4;
71
111
  var OPTIONAL_MARK_FONT_WEIGHT = "400";
72
112
  var styles = reactNative.StyleSheet.create({
73
- container: {
74
- marginBottom: CONTAINER_MARGIN_BOTTOM
75
- },
76
- label: {
77
- marginBottom: LABEL_MARGIN_BOTTOM
78
- },
79
113
  hintText: {
80
- fontSize: HINT_FONT_SIZE,
81
- marginTop: HINT_MARGIN_TOP
114
+ fontSize: HINT_FONT_SIZE
82
115
  },
83
116
  errorText: {
84
- fontSize: ERROR_FONT_SIZE,
85
- marginTop: ERROR_MARGIN_TOP
117
+ fontSize: ERROR_FONT_SIZE
86
118
  },
87
119
  optionalMark: {
88
120
  fontWeight: OPTIONAL_MARK_FONT_WEIGHT
@@ -118,9 +150,12 @@ var Field = ({
118
150
  labelVariant,
119
151
  labelStyle,
120
152
  containerStyle,
153
+ spacing,
121
154
  testID
122
155
  }) => {
123
156
  const { theme } = uiFeedback.useUi();
157
+ const gapOwned = useIsGapOwned();
158
+ const spacingStyles = fieldSpacingStyleSet(resolveFieldSpacing(spacing, gapOwned));
124
159
  const { colors, semantic } = theme;
125
160
  const errorColor = semantic.error["500"];
126
161
  const hasError = hasFieldError(error);
@@ -142,7 +177,7 @@ var Field = ({
142
177
  );
143
178
  const describedById = joinDescribedBy(hasHint ? hintId : void 0, hasError ? errorId : void 0);
144
179
  const control = typeof children === "function" ? children({ describedById, hasError }) : children;
145
- return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.container, containerStyle], testID, children: [
180
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [spacingStyles.container, containerStyle], testID, children: [
146
181
  hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(
147
182
  FieldLabel,
148
183
  {
@@ -151,16 +186,107 @@ var Field = ({
151
186
  optionalLabel,
152
187
  optionalMarkStyle: [styles.optionalMark, themeStyles.optionalMark],
153
188
  requiredMarkStyle: themeStyles.requiredMark,
154
- style: [styles.label, resolveLabelVariantStyle(labelVariant), themeStyles.label, labelStyle]
189
+ style: [spacingStyles.label, resolveLabelVariantStyle(labelVariant), themeStyles.label, labelStyle]
155
190
  }
156
191
  ) : null,
157
192
  control,
158
- hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: hintId, style: [styles.hintText, themeStyles.hintText], children: hint }) : null,
159
- hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
193
+ hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: hintId, style: [styles.hintText, spacingStyles.child, themeStyles.hintText], children: hint }) : null,
194
+ hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: errorId, role: "alert", style: [styles.errorText, spacingStyles.child, themeStyles.errorText], children: error }) : null
160
195
  ] });
161
196
  };
162
- var ACTIONS_GAP = 12;
197
+ var FORM_GRID_DEFAULT_GAP = 12;
198
+ var FORM_GRID_DEFAULT_MIN_WIDTH = 220;
199
+ var FORM_GRID_DEFAULT_BASIS = 260;
163
200
  var styles2 = reactNative.StyleSheet.create({
201
+ grid: {
202
+ flexDirection: "row",
203
+ flexWrap: "wrap"
204
+ }
205
+ });
206
+ var FormGrid = ({
207
+ children,
208
+ gap = FORM_GRID_DEFAULT_GAP,
209
+ minWidth = FORM_GRID_DEFAULT_MIN_WIDTH,
210
+ basis = FORM_GRID_DEFAULT_BASIS,
211
+ style,
212
+ testID
213
+ }) => {
214
+ const metrics = React__default.default.useMemo(() => ({ minWidth, basis }), [minWidth, basis]);
215
+ return /* @__PURE__ */ jsxRuntime.jsx(FormGridProvider, { value: metrics, children: /* @__PURE__ */ jsxRuntime.jsx(GapOwnedProvider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.grid, { gap }, style], testID, children }) }) });
216
+ };
217
+ var GROW = 1;
218
+ var NO_GROW = 0;
219
+ var FormCell = ({
220
+ children,
221
+ minWidth,
222
+ basis,
223
+ grow = true,
224
+ style,
225
+ testID
226
+ }) => {
227
+ const metrics = useFormGridMetrics();
228
+ const resolvedMinWidth = minWidth ?? metrics?.minWidth ?? FORM_GRID_DEFAULT_MIN_WIDTH;
229
+ const resolvedBasis = basis ?? metrics?.basis ?? FORM_GRID_DEFAULT_BASIS;
230
+ const cellStyle = {
231
+ minWidth: resolvedMinWidth,
232
+ flexBasis: resolvedBasis,
233
+ flexGrow: grow ? GROW : NO_GROW
234
+ };
235
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [cellStyle, style], testID, children });
236
+ };
237
+ var SECTION_GAP = 10;
238
+ var DESCRIPTION_FONT_SIZE = 12;
239
+ var styles3 = reactNative.StyleSheet.create({
240
+ section: {
241
+ gap: SECTION_GAP
242
+ },
243
+ description: {
244
+ fontSize: DESCRIPTION_FONT_SIZE
245
+ }
246
+ });
247
+ var sectionSeq = 0;
248
+ function useSectionLegendId() {
249
+ const [id] = React__default.default.useState(() => {
250
+ sectionSeq += 1;
251
+ return `form-section-legend-${String(sectionSeq)}`;
252
+ });
253
+ return id;
254
+ }
255
+ var FormSection = ({
256
+ title,
257
+ description,
258
+ children,
259
+ labelVariant,
260
+ titleStyle,
261
+ style,
262
+ testID
263
+ }) => {
264
+ const { theme } = uiFeedback.useUi();
265
+ const { colors } = theme;
266
+ const legendId = useSectionLegendId();
267
+ const hasTitle = typeof title === "string" && title !== "";
268
+ const hasDescription = typeof description === "string" && description !== "";
269
+ const a11y = { "aria-labelledby": hasTitle ? legendId : void 0 };
270
+ return (
271
+ // The section spaces its children with `gap`, so it owns their vertical rhythm exactly as a
272
+ // `FormGrid` owns a row's — without this a `Field` placed directly in a section would stack its
273
+ // 16px margin on the section's gap and recreate the F3 bug one level up.
274
+ /* @__PURE__ */ jsxRuntime.jsx(GapOwnedProvider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { role: "group", style: [styles3.section, style], testID, ...a11y, children: [
275
+ hasTitle ? /* @__PURE__ */ jsxRuntime.jsx(
276
+ reactNative.Text,
277
+ {
278
+ nativeID: legendId,
279
+ style: [resolveLabelVariantStyle(labelVariant), { color: colors.text }, titleStyle],
280
+ children: title
281
+ }
282
+ ) : null,
283
+ hasDescription ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles3.description, { color: colors.textSecondary }], children: description }) : null,
284
+ children
285
+ ] }) })
286
+ );
287
+ };
288
+ var ACTIONS_GAP = 12;
289
+ var styles4 = reactNative.StyleSheet.create({
164
290
  container: {
165
291
  flexDirection: "row",
166
292
  justifyContent: "flex-end",
@@ -180,7 +306,7 @@ var FormActions = ({
180
306
  saveTestID = "save-button",
181
307
  cancelTestID = "cancel-button",
182
308
  testID
183
- }) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles2.container, containerStyle], testID, children: [
309
+ }) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles4.container, containerStyle], testID, children: [
184
310
  typeof onCancel === "function" ? /* @__PURE__ */ jsxRuntime.jsx(
185
311
  uiButtons.Button,
186
312
  {
@@ -333,7 +459,7 @@ var INPUT_PADDING_V = 10;
333
459
  var INPUT_PADDING_H = 12;
334
460
  var INPUT_BORDER_RADIUS = 8;
335
461
  var INPUT_BORDER_WIDTH = 1;
336
- var styles3 = reactNative.StyleSheet.create({
462
+ var styles5 = reactNative.StyleSheet.create({
337
463
  input: {
338
464
  paddingVertical: INPUT_PADDING_V,
339
465
  paddingHorizontal: INPUT_PADDING_H,
@@ -355,7 +481,7 @@ var FormField = ({
355
481
  describedById,
356
482
  hasError,
357
483
  requiredField: required,
358
- style: styles3.input,
484
+ style: styles5.input,
359
485
  testID: "form-field-input",
360
486
  ...textInputProps
361
487
  }
@@ -393,18 +519,22 @@ function withAlpha2(hex, alpha) {
393
519
  const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
394
520
  return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
395
521
  }
396
- var styles4 = reactNative.StyleSheet.create({
522
+ var styles6 = reactNative.StyleSheet.create({
397
523
  chipContainer: {
398
524
  flexDirection: "row",
399
525
  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
404
- },
405
- chipWrapper: {
406
- marginRight: CHIP_GUTTER,
407
- marginBottom: CHIP_GUTTER
526
+ // `gap`, NOT per-chip margins plus a compensating negative container margin.
527
+ //
528
+ // The old form was `chipWrapper { marginRight: 8, marginBottom: 8 }` with
529
+ // `chipContainer { marginBottom: -8 }`. Its comment blamed `Field`'s 16px container margin —
530
+ // that was WRONG, and worth recording: F3 was briefed on the belief that the -8 compensated for
531
+ // `Field` and had to move in lockstep with it. It did not. -8 cancelled the CHIP's own gutter
532
+ // hanging off the last row, identically whether `Field` contributed 16, 0, or anything else.
533
+ //
534
+ // The pair is still the wrong tool: it holds only while the two constants agree, and it leaves
535
+ // a real negative margin any measuring parent inherits. `gap` produces the byte-identical box
536
+ // and is correct under BOTH of `Field`'s spacing models, not just the one it was tuned against.
537
+ gap: CHIP_GUTTER
408
538
  },
409
539
  chip: {
410
540
  paddingHorizontal: 12,
@@ -456,12 +586,11 @@ function OutlineChip({
456
586
  "aria-pressed": ariaPressed,
457
587
  disabled,
458
588
  hitSlop: CHIP_HIT_SLOP,
459
- style: styles4.chipWrapper,
460
589
  testID,
461
590
  onHoverIn: () => setHovered(true),
462
591
  onHoverOut: () => setHovered(false),
463
592
  onPress,
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 }) })
593
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles6.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.chipText, styles6.chipTextOutline, { color: textColor }], children: option.label }) })
465
594
  }
466
595
  );
467
596
  }
@@ -487,10 +616,9 @@ function SolidChip({
487
616
  "aria-pressed": ariaPressed,
488
617
  disabled,
489
618
  hitSlop: CHIP_HIT_SLOP,
490
- style: styles4.chipWrapper,
491
619
  testID,
492
620
  onPress,
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 }) })
621
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles6.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles6.chipText, { color: textColor }], children: option.label }) })
494
622
  }
495
623
  );
496
624
  }
@@ -508,6 +636,7 @@ var ChipSelector = ({
508
636
  disabled = false,
509
637
  variant = "solid",
510
638
  containerStyle,
639
+ spacing,
511
640
  required = false,
512
641
  error,
513
642
  testID,
@@ -550,10 +679,21 @@ var ChipSelector = ({
550
679
  const key = String(option.value);
551
680
  return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(OutlineChip, { ...chipProps }, key) : /* @__PURE__ */ jsxRuntime.jsx(SolidChip, { ...chipProps }, key);
552
681
  }
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) }) });
682
+ return /* @__PURE__ */ jsxRuntime.jsx(
683
+ Field,
684
+ {
685
+ containerStyle,
686
+ error,
687
+ label,
688
+ required,
689
+ spacing,
690
+ testID,
691
+ children: ({ describedById, hasError }) => /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles6.chipContainer, ...groupA11yProps(describedById, hasError), children: options.map(renderChip) })
692
+ }
693
+ );
554
694
  };
555
- var DESCRIPTION_FONT_SIZE = 12;
556
- var styles5 = reactNative.StyleSheet.create({
695
+ var DESCRIPTION_FONT_SIZE2 = 12;
696
+ var styles7 = reactNative.StyleSheet.create({
557
697
  container: {
558
698
  flexDirection: "row",
559
699
  justifyContent: "space-between",
@@ -567,7 +707,7 @@ var styles5 = reactNative.StyleSheet.create({
567
707
  fontWeight: "600"
568
708
  },
569
709
  description: {
570
- fontSize: DESCRIPTION_FONT_SIZE,
710
+ fontSize: DESCRIPTION_FONT_SIZE2,
571
711
  marginTop: 2
572
712
  }
573
713
  });
@@ -593,13 +733,13 @@ var FormSwitch = ({
593
733
  reactNative.View,
594
734
  {
595
735
  style: [
596
- styles5.container,
736
+ styles7.container,
597
737
  containerStyle
598
738
  ],
599
739
  children: [
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
740
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles7.textColumn, children: [
741
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.label, themeStyles.label], children: label }),
742
+ hasDescription ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles7.description, themeStyles.description], children: description }) : null
603
743
  ] }),
604
744
  /* @__PURE__ */ jsxRuntime.jsx(
605
745
  reactNative.Switch,
@@ -624,7 +764,7 @@ var BOX_SIZE = 20;
624
764
  var BOX_RADIUS = 5;
625
765
  var CHECK_FONT_SIZE = 14;
626
766
  var DISABLED_OPACITY = 0.5;
627
- var styles6 = reactNative.StyleSheet.create({
767
+ var styles8 = reactNative.StyleSheet.create({
628
768
  container: {
629
769
  flexDirection: "row",
630
770
  alignItems: "flex-start",
@@ -706,14 +846,14 @@ var FormCheckbox = ({
706
846
  disabled,
707
847
  onPress: toggle,
708
848
  role: "checkbox",
709
- style: [styles6.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
849
+ style: [styles8.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
710
850
  testID,
711
851
  ...keyboardProps,
712
852
  children: [
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
853
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles8.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
854
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles8.textColumn, children: [
855
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.label, themeStyles.label], children: label }),
856
+ hasHint ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles8.hint, themeStyles.hint], children: hintText }) : null
717
857
  ] })
718
858
  ]
719
859
  }
@@ -1127,8 +1267,11 @@ exports.DEFAULT_TYPEAHEAD_MIN_CHARS = DEFAULT_TYPEAHEAD_MIN_CHARS;
1127
1267
  exports.DateRangeControl = DateRangeControl;
1128
1268
  exports.Field = Field;
1129
1269
  exports.FormActions = FormActions;
1270
+ exports.FormCell = FormCell;
1130
1271
  exports.FormCheckbox = FormCheckbox;
1131
1272
  exports.FormField = FormField;
1273
+ exports.FormGrid = FormGrid;
1274
+ exports.FormSection = FormSection;
1132
1275
  exports.FormSwitch = FormSwitch;
1133
1276
  exports.SelectControl = SelectControl;
1134
1277
  exports.ThemedTextInput = ThemedTextInput;