@fogpipe/forma-react 0.8.2 → 0.10.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.
@@ -74,7 +74,7 @@ function DefaultLayout({ children, onSubmit, isSubmitting }: LayoutProps) {
74
74
  /**
75
75
  * Default field wrapper component with accessibility support
76
76
  */
77
- function DefaultFieldWrapper({ fieldPath, field, children, errors, required, visible }: FieldWrapperProps) {
77
+ function DefaultFieldWrapper({ fieldPath, field, children, errors, showRequiredIndicator, visible }: FieldWrapperProps) {
78
78
  if (!visible) return null;
79
79
 
80
80
  const errorId = `${fieldPath}-error`;
@@ -86,8 +86,8 @@ function DefaultFieldWrapper({ fieldPath, field, children, errors, required, vis
86
86
  {field.label && (
87
87
  <label htmlFor={fieldPath}>
88
88
  {field.label}
89
- {required && <span className="required" aria-hidden="true">*</span>}
90
- {required && <span className="sr-only"> (required)</span>}
89
+ {showRequiredIndicator && <span className="required" aria-hidden="true">*</span>}
90
+ {showRequiredIndicator && <span className="sr-only"> (required)</span>}
91
91
  </label>
92
92
  )}
93
93
  {children}
@@ -274,6 +274,13 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
274
274
  // Get schema property for additional constraints
275
275
  const schemaProperty = spec.schema.properties[fieldPath];
276
276
 
277
+ // Boolean fields: hide asterisk unless they have validation rules (consent pattern)
278
+ // - Binary question ("Do you smoke?"): no validation → false is valid → hide asterisk
279
+ // - Consent checkbox ("I accept terms"): has validation rule → show asterisk
280
+ const isBooleanField = schemaProperty?.type === "boolean" || fieldDef?.type === "boolean";
281
+ const hasValidationRules = (fieldDef?.validations?.length ?? 0) > 0;
282
+ const showRequiredIndicator = required && (!isBooleanField || hasValidationRules);
283
+
277
284
  // Base field props
278
285
  const baseProps: BaseFieldProps = {
279
286
  name: fieldPath,
@@ -429,6 +436,7 @@ export const FormRenderer = forwardRef<FormRendererHandle, FormRendererProps>(
429
436
  errors={errors}
430
437
  touched={touched}
431
438
  required={required}
439
+ showRequiredIndicator={showRequiredIndicator}
432
440
  visible={isVisible}
433
441
  >
434
442
  {React.createElement(Component as React.ComponentType<typeof componentProps>, componentProps)}
@@ -249,10 +249,12 @@ describe("canProceed", () => {
249
249
  expect(result.current.wizard?.canProceed).toBe(true);
250
250
  });
251
251
 
252
- it("required boolean fields - undefined vs false", () => {
253
- // Note: For boolean fields, "required" means "must have a value" (true or false),
254
- // NOT "must be true". This is consistent with other field types where required
255
- // means "not empty". For checkboxes that must be checked (like "Accept Terms"),
252
+ it("required boolean fields - auto-initialized to false", () => {
253
+ // Boolean fields are auto-initialized to false, which is a valid value.
254
+ // This provides better UX - the form is valid from the start since
255
+ // false is a valid answer to "Do you have pets?"
256
+ //
257
+ // For checkboxes that must be checked (like "Accept Terms"),
256
258
  // use a validation rule: { rule: "value = true", message: "Must accept terms" }
257
259
  const spec = createTestSpec({
258
260
  fields: {
@@ -263,13 +265,14 @@ describe("canProceed", () => {
263
265
  ],
264
266
  });
265
267
 
266
- // undefined should be invalid (user hasn't answered)
267
- const { result: resultUndefined } = renderHook(() =>
268
+ // With auto-initialization, boolean defaults to false (a valid value)
269
+ const { result: resultDefault } = renderHook(() =>
268
270
  useForma({ spec, initialData: {} })
269
271
  );
270
- expect(resultUndefined.current.wizard?.canProceed).toBe(false);
272
+ expect(resultDefault.current.data.hasPets).toBe(false);
273
+ expect(resultDefault.current.wizard?.canProceed).toBe(true); // false is valid
271
274
 
272
- // false should be valid (user answered "no")
275
+ // explicit false should be valid (user answered "no")
273
276
  const { result: resultFalse } = renderHook(() =>
274
277
  useForma({ spec, initialData: { hasPets: false } })
275
278
  );