Skip to content

Conversation

@AlbertSmit
Copy link

@AlbertSmit AlbertSmit commented Dec 5, 2025

Summary by CodeRabbit

  • New Features

    • Added FormErrorRegion component for accessible form error display
    • Added focusFirstError utility to focus the first invalid form field
    • Form fields now support ref forwarding for direct input element access
  • Bug Fixes

    • Form submission now focuses the first validation error
  • Documentation

    • Updated API documentation to reflect new components and helpers

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds accessibility features to enable form error handling: a FormErrorRegion component for displaying errors via a live region, and a focusFirstError function to focus the first invalid field. Infrastructure changes include ref forwarding through form field hooks and input components to support programmatic focus.

Changes

Cohort / File(s) Summary
Accessibility utilities
src/a11y.js
Introduces FormErrorRegion (live region component for error display) and focusFirstError (function to locate and focus first error field). Implements internal helpers for error extraction, DOM ordering, and rendering.
Ref propagation infrastructure
src/fields.js, src/hooks.js, example/src/domain/machinery/Form.js
Adds stable ref creation via createStableRef() in basic form fields. Extends useFormField, useNumberFormField, and useBooleanFormField hooks to propagate ref. Updates FormTextInput, FormNumberInput, FormCheckbox, and InputBase to forward ref to underlying input elements.
Public API exports
index.js
Exports FormErrorRegion and focusFirstError from ./src/a11y.
Documentation
readme.md
Documents FormErrorRegion component and focusFirstError function. Updates useFormField signature to reflect new ref return value. Includes usage examples.
Example usage
example/src/domain/Full.js
Imports and calls focusFirstError(form) on form submit when snapshot is invalid.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • src/a11y.js — New file with multiple helper functions and components; logic for DOM ordering and error extraction warrants careful verification
  • Ref propagation across multiple files — Trace ref flow through hooks and components to ensure correct forwarding without breaking existing functionality
  • Hook signature changes — Verify that ref propagation in useFormField, useNumberFormField, and useBooleanFormField maintains backward compatibility and doesn't affect existing consumers

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'A11y focus refs' directly relates to the main changes: adding accessibility features (a11y) for form focus management through refs, including FormErrorRegion, focusFirstError, and ref propagation throughout form components.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/a11y.js (1)

42-47: Consider handling equal nodes in the comparator.

When nodeA === nodeB, compareDocumentPosition returns 0, so the bitwise check fails and the function returns 1. For a stable sort, equal elements should return 0. This is unlikely in practice but worth handling for correctness.

 function byDomOrder(a, b) {
   const nodeA = a.ref.current
   const nodeB = b.ref.current
   if (!nodeA || !nodeB) return 0
+  if (nodeA === nodeB) return 0
   return nodeAPrecedesNodeB(nodeA, nodeB) ? -1 : 1
 }
readme.md (1)

173-184: useFormField ref docs look consistent; consider explicitly tying it to the DOM element

The additions documenting ref in the useFormField return shape and output table read well and match the intended behavior (“pass this ref to the form field element to support focusFirstError” and “only available for basic fields”). This is a solid, minimal description.

Optionally, you could make it even clearer that “form field element” means the actual DOM control (e.g. <input>, <select>, etc.), not a higher‑level wrapper component, to avoid people accidentally attaching the ref to a custom component instead of the native element. For example:

ref – A ref object { current: null } that should be passed to the underlying form field DOM element (e.g. <input>, <select>), …

Purely a clarity tweak; the current text is already usable.

Also applies to: 186-202

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c544dc and 74285ab.

📒 Files selected for processing (7)
  • example/src/domain/Full.js (2 hunks)
  • example/src/domain/machinery/Form.js (3 hunks)
  • index.js (1 hunks)
  • readme.md (4 hunks)
  • src/a11y.js (1 hunks)
  • src/fields.js (3 hunks)
  • src/hooks.js (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
example/src/domain/Full.js (2)
src/a11y.js (1)
  • focusFirstError (23-29)
src/hooks.js (1)
  • form (16-20)
src/a11y.js (2)
src/hooks.js (8)
  • useFormFieldSnapshot (81-90)
  • field (102-102)
  • field (133-133)
  • field (140-140)
  • state (82-88)
  • state (103-103)
  • state (134-134)
  • state (141-141)
src/validation.js (1)
  • error (22-22)
🪛 markdownlint-cli2 (0.18.1)
readme.md

113-113: Link fragments should be valid
Expected: #formerrorregion; Actual: #FormErrorRegion

(MD051, link-fragments)


115-115: Link fragments should be valid
Expected: #focusfirsterror; Actual: #focusFirstError

(MD051, link-fragments)

🔇 Additional comments (10)
src/a11y.js (3)

57-70: LGTM!

The flattenErrors function correctly handles various error structures: null/undefined, primitive errors, error objects with id/params, and nested structures with self/children.


6-17: LGTM!

The FormErrorRegion component correctly implements a visually hidden live region with aria-live="polite" and role="status", which is the appropriate pattern for announcing form errors to screen readers without interrupting.


72-86: LGTM!

The defaultRenderError gracefully handles different error shapes, and visuallyHiddenStyle follows the standard accessible hiding technique.

example/src/domain/Full.js (1)

80-84: LGTM!

Good example of the intended usage pattern: checking snapshot.invalid before calling focusFirstError(form) and returning early to prevent submission with invalid data.

index.js (1)

17-20: LGTM!

Clean addition of the new accessibility utilities to the public API, following the existing export pattern.

src/hooks.js (2)

108-125: LGTM!

Consistent ref propagation through useNumberFormField and useBooleanFormField, maintaining the same pattern as useFormField.


100-106: No action needed. The ref property is correctly available whenever these hooks are called. The useNumberFormField and useBooleanFormField hooks are designed to work only with basic fields, which always have a ref property created by createStableRef(). Object and array fields have different structures (fields and helpers properties instead of eventHandlers) and would never be passed to these hooks.

src/fields.js (2)

142-143: LGTM!

Creating the ref at field construction time ensures stable identity for the lifetime of the field, which is the correct approach for programmatic focus management.


243-261: LGTM!

Excellent documentation explaining why a plain object works as a ref outside React components. The implementation is minimal and correct.

readme.md (1)

452-485: New FormErrorRegion and focusFirstError docs are clear and align with the new a11y feature

The sections for FormErrorRegion and focusFirstError concisely explain:

  • what each does (live region for errors vs. focusing first invalid field),
  • required props (form, optional renderError),
  • and typical usage on failed submit (if (snapshot.invalid) { focusFirstError(form) }).

This is enough for consumers to adopt the new a11y helpers without digging into implementation. No changes strictly needed from a docs perspective.

@AlbertSmit AlbertSmit marked this pull request as draft December 5, 2025 09:55
…and add optional chaining to `focusFirstError`.
@EECOLOR
Copy link
Member

EECOLOR commented Dec 5, 2025

@AlbertSmit I think this is easier:

import { flushSync } from 'react-dom'

export function moveFocusToFirstErrorField(formRef) {
  const formElement = formRef.current
  if (!formElement) {
    return
  }

  // make sure dom is updated after validation so fields with error have got identifying data-attribute
  flushSync(() => {})

  const firstErrorField = formElement.querySelector('[data-form-field-has-error="true"]')
  if (firstErrorField && (typeof firstErrorField.focus === 'function')) {
    firstErrorField.focus()
  }
}

@EECOLOR
Copy link
Member

EECOLOR commented Dec 5, 2025

Also, please base your pull request on typed :-D

@AlbertSmit
Copy link
Author

@AlbertSmit I think this is easier:

import { flushSync } from 'react-dom'

export function moveFocusToFirstErrorField(formRef) {
  const formElement = formRef.current
  if (!formElement) {
    return
  }

  // make sure dom is updated after validation so fields with error have got identifying data-attribute
  flushSync(() => {})

  const firstErrorField = formElement.querySelector('[data-form-field-has-error="true"]')
  if (firstErrorField && (typeof firstErrorField.focus === 'function')) {
    firstErrorField.focus()
  }
}

Hmm, ja, misschien wel minder code qua regels, maar niet perse de React-way of doing things?

Voorbeeldimplementatie van data-form-field-has-error:
https://github.com/Kaliber/icm/blob/ce7d1c259841c2808b2ac8baea52ed8e1a54a740/wordpress/src/react/forms/shared/FormField.js#L52C9-L52C62

Wellicht ook wat breekbaar qua opzet; in data-form-field-has-error={showError ? 'true' : null} is er nu;

  • een data-attribuut dat niet strict is (tenzij je hem typed in e.g. global.d.ts, maar dan nog)
  • een boolean waarde die je als string moet definieëren
  • een fallback naar een niet boolean waarde (ook niet als string type)

Ik kan me voorstellen dat dat gaat verwarren in de toekomst. Ik snap wel dat dat is hoe data-attributen werken (bijvoorbeeld bij inert loop je tegen hetzelfde gedoe aan), maar ook daar is dat sub-optimaal in de React wereld, in mijn ogen.

Bij het gebruik van ref blijf je in de React wereld, en krijg je als bonus een ref cadeau in je form field, voor als je in de toekomst nog andere dingen met de input wilt doen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants