@mittwald/flow-react-components 1.4.33 → 1.4.34

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/AGENTS.md CHANGED
@@ -400,7 +400,76 @@ Four traps cost time in every new browser test:
400
400
  `wrapperProps` go on the outer element, `controlProps` on what assistive
401
401
  technology lands on (the input or the group), spread after `rest`: its
402
402
  `aria-describedby` references the error only while one is rendered and already
403
- contains the consumer's own ids.
403
+ contains the consumer's own ids. See
404
+ [Building a form field](#building-a-form-field).
405
+
406
+ ## Building a form field
407
+
408
+ A form field is any component a user enters a value with — anything that calls
409
+ `useFieldComponent`. It supports the whole contract below, end to end: prop →
410
+ behavior → styling → `Label` state → test. Gaps in existing fields are tracked
411
+ in #3368.
412
+
413
+ | Aspect | Requirement |
414
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
415
+ | `isDisabled` | Control is not operable (`disabled` or `aria-disabled`), disabled styling via the `formControl` mixin, `Label` disabled. Every inner button too. |
416
+ | `isReadOnly` | Value cannot change, control still focusable and submitted. Native `readonly` / react-aria's `isReadOnly` where it exists, otherwise `aria-readonly` — never by disabling the control. Inner buttons that change the value off. |
417
+ | `isRequired` | `required` or `aria-required` on the control; `Label` drops its "optional" marker (`useFieldComponent` does that). |
418
+ | `isInvalid` | `aria-invalid` on the control, invalid styling, `FieldError` rendered and linked. |
419
+ | `validationBehavior` | `"native"` blocks form submission on a constraint violation, `"aria"` does not. react-hook-form's `Field` passes `"aria"`. |
420
+ | `Label` / `FieldDescription` | Children slots via `useFieldComponent`; `Label` names the control. `controlProps` (with `aria-describedby`) land on the **focusable control** (or the group of a grouped control), not on a wrapper `div`. |
421
+ | `name` / `form` | The value is in the `FormData` of the surrounding (or `form`-referenced) form. A custom control renders a hidden input. |
422
+ | `defaultValue` / `value` | Both work; `onChange` reports user changes with the field's own value type (a `number` field reports a `number`). |
423
+ | `ref` | Points at the focusable control — react-hook-form focuses it on a validation error. |
424
+ | `autoFocus`, `onFocus`/`onBlur` | Reach the focusable control. react-hook-form marks a field touched via `onBlur`. |
425
+
426
+ An exception needs a reason that holds for the field's nature (a `Slider` always
427
+ has a value, so it has no required state) — not "not implemented yet".
428
+
429
+ Checklist beyond the component itself:
430
+
431
+ - **Contract test.** Call `testFormFieldContract`
432
+ (`src/tests/formFieldContract/`) in the field's `*.browser.test.tsx`. The
433
+ adapter renders the field, names its control, two values (the first one
434
+ submits an entry, the two submit different entries), their `FormData` entries
435
+ and a user interaction that changes the value. The interaction passes `force`
436
+ on, so the test can also try it on a disabled or read-only field.
437
+ `getValueButtons` narrows the inner buttons a read-only field turns off; it
438
+ defaults to all. Justified exceptions go in `exceptions` with their reason. An
439
+ excepted aspect must fail on one of its own expectations — once it passes, or
440
+ fails on a lookup or a timeout instead, the run fails:
441
+
442
+ ```tsx
443
+ testFormFieldContract("TextField", {
444
+ render: (props) => <TextField {...props} />,
445
+ getControl: (screen) => screen.getByRole("textbox"),
446
+ values: ["foo", "bar"],
447
+ toFormValue: (value) => value,
448
+ changeValue: async (screen, { force }) => {
449
+ await userEvent.fill(screen.getByRole("textbox"), "bar", { force });
450
+ },
451
+ });
452
+ ```
453
+
454
+ It checks behavior and ARIA, not looks — the disabled, read-only and invalid
455
+ states also get scenarios in the field's visual test
456
+ (`packages/remote-react-components/src/tests/visual`).
457
+
458
+ - **react-hook-form.** Register the field in the props context of
459
+ `src/integrations/react-hook-form/components/Field/Field.tsx`. `Field` passes
460
+ `name`, `value` and `defaultValue` together, `onChange`, `onBlur`, `ref`,
461
+ `form`, `isRequired`, `isReadOnly`, `isInvalid` and `validationBehavior` —
462
+ spread the rest props through instead of destructuring a fixed list, or they
463
+ are dropped silently. A field whose value prop is not `value` gets it mapped
464
+ there (`selectedKey` for `ComboBox`, `isSelected` for `Checkbox`). `Field`
465
+ does not pass `isDisabled` yet (#3368).
466
+ - **Remote.** A field that mirrors its value with `useControlledHostValueProps`
467
+ passes the type's empty value (see the controlled-from-the-first-render
468
+ convention below). A text input among them also needs its `flr-*` tag in
469
+ `controlledComponentNames`
470
+ (`packages/remote-react-components/src/lib/createRemoteComponent.ts`), or it
471
+ drops characters under fast typing
472
+ ([docs/remote-ui.md](https://github.com/mittwald/flow/blob/main/docs/remote-ui.md)).
404
473
 
405
474
  ## Public API surfaces
406
475