@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.mjs CHANGED
@@ -36,6 +36,40 @@ function resolveFieldMarker(required, optional) {
36
36
  if (optional) return "optional";
37
37
  return "none";
38
38
  }
39
+ var FIELD_STACK_MARGIN_BOTTOM = 16;
40
+ var FIELD_STACK_LABEL_MARGIN_BOTTOM = 4;
41
+ var FIELD_STACK_CHILD_MARGIN_TOP = 4;
42
+ var FIELD_CONTRACT_GAP = 6;
43
+ var fieldSpacingStyles = StyleSheet.create({
44
+ stackContainer: { marginBottom: FIELD_STACK_MARGIN_BOTTOM },
45
+ stackLabel: { marginBottom: FIELD_STACK_LABEL_MARGIN_BOTTOM },
46
+ stackChild: { marginTop: FIELD_STACK_CHILD_MARGIN_TOP },
47
+ /**
48
+ * The gap model sets NO margins — not even zeroed ones. An explicit `marginBottom: 0` here would
49
+ * defeat a caller who legitimately wants to push one field down via `containerStyle`, because
50
+ * `containerStyle` composes AFTER this and a zero is indistinguishable from "unset" to a reader.
51
+ */
52
+ gapContainer: { gap: FIELD_CONTRACT_GAP },
53
+ none: {}
54
+ });
55
+ function resolveFieldSpacing(explicit, insideGrid) {
56
+ if (explicit !== void 0) return explicit;
57
+ return insideGrid ? "gap" : "stack";
58
+ }
59
+ function fieldSpacingStyleSet(spacing) {
60
+ if (spacing === "gap") {
61
+ return {
62
+ container: fieldSpacingStyles.gapContainer,
63
+ label: fieldSpacingStyles.none,
64
+ child: fieldSpacingStyles.none
65
+ };
66
+ }
67
+ return {
68
+ container: fieldSpacingStyles.stackContainer,
69
+ label: fieldSpacingStyles.stackLabel,
70
+ child: fieldSpacingStyles.stackChild
71
+ };
72
+ }
39
73
  var FIELD_FONT_SIZE = 13;
40
74
  var FIELD_FONT_WEIGHT = "600";
41
75
  var CONTROL_FONT_SIZE = 11;
@@ -56,27 +90,25 @@ var labelVariantStyles = StyleSheet.create({
56
90
  function resolveLabelVariantStyle(variant) {
57
91
  return variant === "control" ? labelVariantStyles.control : labelVariantStyles.field;
58
92
  }
59
- var CONTAINER_MARGIN_BOTTOM = 16;
60
- var LABEL_MARGIN_BOTTOM = 4;
93
+ var FormGridContext = React.createContext(null);
94
+ var GapOwnedContext = React.createContext(false);
95
+ var FormGridProvider = FormGridContext.Provider;
96
+ var GapOwnedProvider = GapOwnedContext.Provider;
97
+ function useFormGridMetrics() {
98
+ return React.useContext(FormGridContext);
99
+ }
100
+ function useIsGapOwned() {
101
+ return React.useContext(GapOwnedContext);
102
+ }
61
103
  var ERROR_FONT_SIZE = 12;
62
- var ERROR_MARGIN_TOP = 4;
63
104
  var HINT_FONT_SIZE = 12;
64
- var HINT_MARGIN_TOP = 4;
65
105
  var OPTIONAL_MARK_FONT_WEIGHT = "400";
66
106
  var styles = StyleSheet.create({
67
- container: {
68
- marginBottom: CONTAINER_MARGIN_BOTTOM
69
- },
70
- label: {
71
- marginBottom: LABEL_MARGIN_BOTTOM
72
- },
73
107
  hintText: {
74
- fontSize: HINT_FONT_SIZE,
75
- marginTop: HINT_MARGIN_TOP
108
+ fontSize: HINT_FONT_SIZE
76
109
  },
77
110
  errorText: {
78
- fontSize: ERROR_FONT_SIZE,
79
- marginTop: ERROR_MARGIN_TOP
111
+ fontSize: ERROR_FONT_SIZE
80
112
  },
81
113
  optionalMark: {
82
114
  fontWeight: OPTIONAL_MARK_FONT_WEIGHT
@@ -112,9 +144,12 @@ var Field = ({
112
144
  labelVariant,
113
145
  labelStyle,
114
146
  containerStyle,
147
+ spacing,
115
148
  testID
116
149
  }) => {
117
150
  const { theme } = useUi();
151
+ const gapOwned = useIsGapOwned();
152
+ const spacingStyles = fieldSpacingStyleSet(resolveFieldSpacing(spacing, gapOwned));
118
153
  const { colors, semantic } = theme;
119
154
  const errorColor = semantic.error["500"];
120
155
  const hasError = hasFieldError(error);
@@ -136,7 +171,7 @@ var Field = ({
136
171
  );
137
172
  const describedById = joinDescribedBy(hasHint ? hintId : void 0, hasError ? errorId : void 0);
138
173
  const control = typeof children === "function" ? children({ describedById, hasError }) : children;
139
- return /* @__PURE__ */ jsxs(View, { style: [styles.container, containerStyle], testID, children: [
174
+ return /* @__PURE__ */ jsxs(View, { style: [spacingStyles.container, containerStyle], testID, children: [
140
175
  hasLabel ? /* @__PURE__ */ jsx(
141
176
  FieldLabel,
142
177
  {
@@ -145,16 +180,107 @@ var Field = ({
145
180
  optionalLabel,
146
181
  optionalMarkStyle: [styles.optionalMark, themeStyles.optionalMark],
147
182
  requiredMarkStyle: themeStyles.requiredMark,
148
- style: [styles.label, resolveLabelVariantStyle(labelVariant), themeStyles.label, labelStyle]
183
+ style: [spacingStyles.label, resolveLabelVariantStyle(labelVariant), themeStyles.label, labelStyle]
149
184
  }
150
185
  ) : null,
151
186
  control,
152
- hasHint ? /* @__PURE__ */ jsx(Text, { nativeID: hintId, style: [styles.hintText, themeStyles.hintText], children: hint }) : null,
153
- hasError ? /* @__PURE__ */ jsx(Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
187
+ hasHint ? /* @__PURE__ */ jsx(Text, { nativeID: hintId, style: [styles.hintText, spacingStyles.child, themeStyles.hintText], children: hint }) : null,
188
+ hasError ? /* @__PURE__ */ jsx(Text, { nativeID: errorId, role: "alert", style: [styles.errorText, spacingStyles.child, themeStyles.errorText], children: error }) : null
154
189
  ] });
155
190
  };
156
- var ACTIONS_GAP = 12;
191
+ var FORM_GRID_DEFAULT_GAP = 12;
192
+ var FORM_GRID_DEFAULT_MIN_WIDTH = 220;
193
+ var FORM_GRID_DEFAULT_BASIS = 260;
157
194
  var styles2 = StyleSheet.create({
195
+ grid: {
196
+ flexDirection: "row",
197
+ flexWrap: "wrap"
198
+ }
199
+ });
200
+ var FormGrid = ({
201
+ children,
202
+ gap = FORM_GRID_DEFAULT_GAP,
203
+ minWidth = FORM_GRID_DEFAULT_MIN_WIDTH,
204
+ basis = FORM_GRID_DEFAULT_BASIS,
205
+ style,
206
+ testID
207
+ }) => {
208
+ const metrics = React.useMemo(() => ({ minWidth, basis }), [minWidth, basis]);
209
+ return /* @__PURE__ */ jsx(FormGridProvider, { value: metrics, children: /* @__PURE__ */ jsx(GapOwnedProvider, { value: true, children: /* @__PURE__ */ jsx(View, { style: [styles2.grid, { gap }, style], testID, children }) }) });
210
+ };
211
+ var GROW = 1;
212
+ var NO_GROW = 0;
213
+ var FormCell = ({
214
+ children,
215
+ minWidth,
216
+ basis,
217
+ grow = true,
218
+ style,
219
+ testID
220
+ }) => {
221
+ const metrics = useFormGridMetrics();
222
+ const resolvedMinWidth = minWidth ?? metrics?.minWidth ?? FORM_GRID_DEFAULT_MIN_WIDTH;
223
+ const resolvedBasis = basis ?? metrics?.basis ?? FORM_GRID_DEFAULT_BASIS;
224
+ const cellStyle = {
225
+ minWidth: resolvedMinWidth,
226
+ flexBasis: resolvedBasis,
227
+ flexGrow: grow ? GROW : NO_GROW
228
+ };
229
+ return /* @__PURE__ */ jsx(View, { style: [cellStyle, style], testID, children });
230
+ };
231
+ var SECTION_GAP = 10;
232
+ var DESCRIPTION_FONT_SIZE = 12;
233
+ var styles3 = StyleSheet.create({
234
+ section: {
235
+ gap: SECTION_GAP
236
+ },
237
+ description: {
238
+ fontSize: DESCRIPTION_FONT_SIZE
239
+ }
240
+ });
241
+ var sectionSeq = 0;
242
+ function useSectionLegendId() {
243
+ const [id] = React.useState(() => {
244
+ sectionSeq += 1;
245
+ return `form-section-legend-${String(sectionSeq)}`;
246
+ });
247
+ return id;
248
+ }
249
+ var FormSection = ({
250
+ title,
251
+ description,
252
+ children,
253
+ labelVariant,
254
+ titleStyle,
255
+ style,
256
+ testID
257
+ }) => {
258
+ const { theme } = useUi();
259
+ const { colors } = theme;
260
+ const legendId = useSectionLegendId();
261
+ const hasTitle = typeof title === "string" && title !== "";
262
+ const hasDescription = typeof description === "string" && description !== "";
263
+ const a11y = { "aria-labelledby": hasTitle ? legendId : void 0 };
264
+ return (
265
+ // The section spaces its children with `gap`, so it owns their vertical rhythm exactly as a
266
+ // `FormGrid` owns a row's — without this a `Field` placed directly in a section would stack its
267
+ // 16px margin on the section's gap and recreate the F3 bug one level up.
268
+ /* @__PURE__ */ jsx(GapOwnedProvider, { value: true, children: /* @__PURE__ */ jsxs(View, { role: "group", style: [styles3.section, style], testID, ...a11y, children: [
269
+ hasTitle ? /* @__PURE__ */ jsx(
270
+ Text,
271
+ {
272
+ nativeID: legendId,
273
+ style: [resolveLabelVariantStyle(labelVariant), { color: colors.text }, titleStyle],
274
+ children: title
275
+ }
276
+ ) : null,
277
+ hasDescription ? /* @__PURE__ */ jsx(Text, { style: [styles3.description, { color: colors.textSecondary }], children: description }) : null,
278
+ children
279
+ ] }) })
280
+ );
281
+ };
282
+ var ACTIONS_GAP = 12;
283
+ var styles4 = StyleSheet.create({
158
284
  container: {
159
285
  flexDirection: "row",
160
286
  justifyContent: "flex-end",
@@ -174,7 +300,7 @@ var FormActions = ({
174
300
  saveTestID = "save-button",
175
301
  cancelTestID = "cancel-button",
176
302
  testID
177
- }) => /* @__PURE__ */ jsxs(View, { style: [styles2.container, containerStyle], testID, children: [
303
+ }) => /* @__PURE__ */ jsxs(View, { style: [styles4.container, containerStyle], testID, children: [
178
304
  typeof onCancel === "function" ? /* @__PURE__ */ jsx(
179
305
  Button,
180
306
  {
@@ -327,7 +453,7 @@ var INPUT_PADDING_V = 10;
327
453
  var INPUT_PADDING_H = 12;
328
454
  var INPUT_BORDER_RADIUS = 8;
329
455
  var INPUT_BORDER_WIDTH = 1;
330
- var styles3 = StyleSheet.create({
456
+ var styles5 = StyleSheet.create({
331
457
  input: {
332
458
  paddingVertical: INPUT_PADDING_V,
333
459
  paddingHorizontal: INPUT_PADDING_H,
@@ -349,7 +475,7 @@ var FormField = ({
349
475
  describedById,
350
476
  hasError,
351
477
  requiredField: required,
352
- style: styles3.input,
478
+ style: styles5.input,
353
479
  testID: "form-field-input",
354
480
  ...textInputProps
355
481
  }
@@ -387,18 +513,22 @@ function withAlpha2(hex, alpha) {
387
513
  const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
388
514
  return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
389
515
  }
390
- var styles4 = StyleSheet.create({
516
+ var styles6 = StyleSheet.create({
391
517
  chipContainer: {
392
518
  flexDirection: "row",
393
519
  flexWrap: "wrap",
394
- // Each chip's bottom gutter also hangs off the LAST row, stacking on `Field`'s container
395
- // margin (8 + 16 = 24) and dropping the block below the `FormField` beside it. Cancelling it
396
- // is standard gutter compensation: inner rows keep their 8px, the block ends at Field's 16.
397
- marginBottom: -CHIP_GUTTER
398
- },
399
- chipWrapper: {
400
- marginRight: CHIP_GUTTER,
401
- marginBottom: CHIP_GUTTER
520
+ // `gap`, NOT per-chip margins plus a compensating negative container margin.
521
+ //
522
+ // The old form was `chipWrapper { marginRight: 8, marginBottom: 8 }` with
523
+ // `chipContainer { marginBottom: -8 }`. Its comment blamed `Field`'s 16px container margin —
524
+ // that was WRONG, and worth recording: F3 was briefed on the belief that the -8 compensated for
525
+ // `Field` and had to move in lockstep with it. It did not. -8 cancelled the CHIP's own gutter
526
+ // hanging off the last row, identically whether `Field` contributed 16, 0, or anything else.
527
+ //
528
+ // The pair is still the wrong tool: it holds only while the two constants agree, and it leaves
529
+ // a real negative margin any measuring parent inherits. `gap` produces the byte-identical box
530
+ // and is correct under BOTH of `Field`'s spacing models, not just the one it was tuned against.
531
+ gap: CHIP_GUTTER
402
532
  },
403
533
  chip: {
404
534
  paddingHorizontal: 12,
@@ -450,12 +580,11 @@ function OutlineChip({
450
580
  "aria-pressed": ariaPressed,
451
581
  disabled,
452
582
  hitSlop: CHIP_HIT_SLOP,
453
- style: styles4.chipWrapper,
454
583
  testID,
455
584
  onHoverIn: () => setHovered(true),
456
585
  onHoverOut: () => setHovered(false),
457
586
  onPress,
458
- children: /* @__PURE__ */ jsx(View, { style: [styles4.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles4.chipText, styles4.chipTextOutline, { color: textColor }], children: option.label }) })
587
+ children: /* @__PURE__ */ jsx(View, { style: [styles6.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles6.chipText, styles6.chipTextOutline, { color: textColor }], children: option.label }) })
459
588
  }
460
589
  );
461
590
  }
@@ -481,10 +610,9 @@ function SolidChip({
481
610
  "aria-pressed": ariaPressed,
482
611
  disabled,
483
612
  hitSlop: CHIP_HIT_SLOP,
484
- style: styles4.chipWrapper,
485
613
  testID,
486
614
  onPress,
487
- children: /* @__PURE__ */ jsx(View, { style: [styles4.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles4.chipText, { color: textColor }], children: option.label }) })
615
+ children: /* @__PURE__ */ jsx(View, { style: [styles6.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsx(Text, { style: [styles6.chipText, { color: textColor }], children: option.label }) })
488
616
  }
489
617
  );
490
618
  }
@@ -502,6 +630,7 @@ var ChipSelector = ({
502
630
  disabled = false,
503
631
  variant = "solid",
504
632
  containerStyle,
633
+ spacing,
505
634
  required = false,
506
635
  error,
507
636
  testID,
@@ -544,10 +673,21 @@ var ChipSelector = ({
544
673
  const key = String(option.value);
545
674
  return variant === "outline" ? /* @__PURE__ */ jsx(OutlineChip, { ...chipProps }, key) : /* @__PURE__ */ jsx(SolidChip, { ...chipProps }, key);
546
675
  }
547
- return /* @__PURE__ */ jsx(Field, { containerStyle, error, label, required, testID, children: ({ describedById, hasError }) => /* @__PURE__ */ jsx(View, { style: styles4.chipContainer, ...groupA11yProps(describedById, hasError), children: options.map(renderChip) }) });
676
+ return /* @__PURE__ */ jsx(
677
+ Field,
678
+ {
679
+ containerStyle,
680
+ error,
681
+ label,
682
+ required,
683
+ spacing,
684
+ testID,
685
+ children: ({ describedById, hasError }) => /* @__PURE__ */ jsx(View, { style: styles6.chipContainer, ...groupA11yProps(describedById, hasError), children: options.map(renderChip) })
686
+ }
687
+ );
548
688
  };
549
- var DESCRIPTION_FONT_SIZE = 12;
550
- var styles5 = StyleSheet.create({
689
+ var DESCRIPTION_FONT_SIZE2 = 12;
690
+ var styles7 = StyleSheet.create({
551
691
  container: {
552
692
  flexDirection: "row",
553
693
  justifyContent: "space-between",
@@ -561,7 +701,7 @@ var styles5 = StyleSheet.create({
561
701
  fontWeight: "600"
562
702
  },
563
703
  description: {
564
- fontSize: DESCRIPTION_FONT_SIZE,
704
+ fontSize: DESCRIPTION_FONT_SIZE2,
565
705
  marginTop: 2
566
706
  }
567
707
  });
@@ -587,13 +727,13 @@ var FormSwitch = ({
587
727
  View,
588
728
  {
589
729
  style: [
590
- styles5.container,
730
+ styles7.container,
591
731
  containerStyle
592
732
  ],
593
733
  children: [
594
- /* @__PURE__ */ jsxs(View, { style: styles5.textColumn, children: [
595
- /* @__PURE__ */ jsx(Text, { style: [styles5.label, themeStyles.label], children: label }),
596
- hasDescription ? /* @__PURE__ */ jsx(Text, { style: [styles5.description, themeStyles.description], children: description }) : null
734
+ /* @__PURE__ */ jsxs(View, { style: styles7.textColumn, children: [
735
+ /* @__PURE__ */ jsx(Text, { style: [styles7.label, themeStyles.label], children: label }),
736
+ hasDescription ? /* @__PURE__ */ jsx(Text, { style: [styles7.description, themeStyles.description], children: description }) : null
597
737
  ] }),
598
738
  /* @__PURE__ */ jsx(
599
739
  Switch,
@@ -618,7 +758,7 @@ var BOX_SIZE = 20;
618
758
  var BOX_RADIUS = 5;
619
759
  var CHECK_FONT_SIZE = 14;
620
760
  var DISABLED_OPACITY = 0.5;
621
- var styles6 = StyleSheet.create({
761
+ var styles8 = StyleSheet.create({
622
762
  container: {
623
763
  flexDirection: "row",
624
764
  alignItems: "flex-start",
@@ -700,14 +840,14 @@ var FormCheckbox = ({
700
840
  disabled,
701
841
  onPress: toggle,
702
842
  role: "checkbox",
703
- style: [styles6.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
843
+ style: [styles8.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
704
844
  testID,
705
845
  ...keyboardProps,
706
846
  children: [
707
- /* @__PURE__ */ jsx(View, { style: [styles6.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsx(Text, { style: [styles6.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
708
- /* @__PURE__ */ jsxs(View, { style: styles6.textColumn, children: [
709
- /* @__PURE__ */ jsx(Text, { style: [styles6.label, themeStyles.label], children: label }),
710
- hasHint ? /* @__PURE__ */ jsx(Text, { style: [styles6.hint, themeStyles.hint], children: hintText }) : null
847
+ /* @__PURE__ */ jsx(View, { style: [styles8.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsx(Text, { style: [styles8.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
848
+ /* @__PURE__ */ jsxs(View, { style: styles8.textColumn, children: [
849
+ /* @__PURE__ */ jsx(Text, { style: [styles8.label, themeStyles.label], children: label }),
850
+ hasHint ? /* @__PURE__ */ jsx(Text, { style: [styles8.hint, themeStyles.hint], children: hintText }) : null
711
851
  ] })
712
852
  ]
713
853
  }
@@ -1113,6 +1253,6 @@ function DateRangeControl({
1113
1253
  ] });
1114
1254
  }
1115
1255
 
1116
- export { AnchoredMenu, ChipSelector, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, Field, FormActions, FormCheckbox, FormField, FormSwitch, SelectControl, ThemedTextInput, TypeaheadControl, controlStyles, isUnmatched, suggestOptions, useThemedInput };
1256
+ export { AnchoredMenu, ChipSelector, DEFAULT_OPTIONAL_LABEL, DEFAULT_TYPEAHEAD_MAX_SUGGESTIONS, DEFAULT_TYPEAHEAD_MIN_CHARS, DateRangeControl, Field, FormActions, FormCell, FormCheckbox, FormField, FormGrid, FormSection, FormSwitch, SelectControl, ThemedTextInput, TypeaheadControl, controlStyles, isUnmatched, suggestOptions, useThemedInput };
1117
1257
  //# sourceMappingURL=index.mjs.map
1118
1258
  //# sourceMappingURL=index.mjs.map