fix(a11y): auto-generate id/name em Input+Textarea + componente LabeledField#435
Conversation
…eld compound component Fixes two DevTools accessibility warnings: 1. 'A form field element should have an id or name attribute' (19 resources) 2. 'No label associated with a form field' (6 resources) Changes: - input.tsx: auto-resolves id from name, or falls back to React.useId() also ensures name attribute is always present (derived from id if not supplied) - textarea.tsx: same pattern as input.tsx - LabeledField.tsx: new compound component that auto-links <Label htmlFor> to <Input id> using a shared React.useId(). Canonical fix for bug #2. Also exports LabeledTextarea for textarea fields. Bug #2 (6 labels without associated field) is solved architecturally via LabeledField. Existing call sites using bare <Label> + <Input> without coordinated htmlFor/id should be migrated to <LabeledField label='...'>. Bug #1 (19 inputs/textareas without id/name) is fixed at the primitive level."
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 36 minutes and 55 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f64a58ec8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| name={name} | ||
| aria-describedby={ariaDescribedBy} | ||
| aria-invalid={!!error} | ||
| aria-required={required} |
There was a problem hiding this comment.
Forward
required to the underlying form control
The component consumes required for label styling but never passes the native required attribute to <Input> (and the textarea variant follows the same pattern), so <LabeledField required> does not trigger built-in browser constraint validation. aria-required only exposes semantics to assistive tech and does not enforce submission rules, which can let empty required fields through unless every caller adds separate validation.
Useful? React with 👍 / 👎.
| const ariaDescribedBy = | ||
| [descriptionId, errorId].filter(Boolean).join(' ') || undefined; |
There was a problem hiding this comment.
Build
aria-describedby only from rendered helper text
When both description and error are set, aria-describedby includes both IDs, but the description element is not rendered (description && !error). That produces a broken ARIA ID reference at runtime and can trigger accessibility audit failures or inconsistent announcements in screen readers; the described-by list should match the elements actually present in the DOM.
Useful? React with 👍 / 👎.
| ) => { | ||
| const autoId = React.useId(); | ||
| // Prefer name-based id for readability in DevTools; fall back to autoId. | ||
| const fieldId = name ? `field-${name}` : autoId; |
There was a problem hiding this comment.
Generate unique ids even when fields share the same name
Deriving fieldId directly from name makes every instance with the same name produce the same DOM id (e.g. repeated sections or same logical field in multiple forms). Duplicate ids break label targeting because htmlFor resolves to the first matching element, so later labels can focus/announce the wrong control; include a per-instance unique suffix or allow caller-provided ids to avoid collisions.
Useful? React with 👍 / 👎.
| ({ className, type, id, name, ...props }, ref) => { | ||
| const fallbackId = React.useId(); | ||
| // Resolve id: explicit > derived from name > unique fallback | ||
| const resolvedId = id ?? name ?? fallbackId; |
There was a problem hiding this comment.
Avoid reusing
name as fallback id in base input primitives
Using name as the default id means controls that intentionally share a name (most notably radio groups) will also share the same id when callers omit id. This creates duplicate ids and breaks explicit <label for=...> associations, since multiple options in one group need the same name but distinct ids.
Useful? React with 👍 / 👎.
| export type LabeledFieldProps = LabeledFieldBaseProps & | ||
| Omit<React.ComponentProps<'input'>, 'id'>; |
There was a problem hiding this comment.
Permit explicit
id in LabeledField prop types
The prop types explicitly remove id (Omit<..., 'id'>), so callers cannot provide a deterministic unique id through normal typing even when they need to prevent collisions (for example repeated fields with the same name). This blocks a practical escape hatch and contradicts the component comment that the id is auto-generated only when not supplied.
Useful? React with 👍 / 👎.
| ({ className, ...props }, ref) => { | ||
| ({ className, id, name, ...props }, ref) => { | ||
| const fallbackId = React.useId(); | ||
| const resolvedId = id ?? name ?? fallbackId; |
There was a problem hiding this comment.
Avoid reusing
name as fallback id in textarea primitive
Textarea now mirrors the input fallback id ?? name ?? useId, which causes duplicate DOM ids whenever multiple textareas intentionally share the same name (e.g. repeated groups or array-style fields). Duplicate ids break label targeting and ARIA references, so id generation should remain unique per element even when names repeat.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Improves form-field accessibility by guaranteeing stable id/name attributes for base inputs and introducing a compound “label + field” component that links labels/descriptions/errors via shared IDs.
Changes:
- Auto-resolve
id/nameforInputandTextareausingprop → name → React.useId()fallback. - Add
LabeledField/LabeledTextareacompound components that bind<Label htmlFor>to the underlying field and wire uparia-*attributes. - Add documentation comments describing accessibility rules and resolution order.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| src/components/ui/textarea.tsx | Auto-generates/derives id and name for <textarea> to satisfy a11y/devtools requirements. |
| src/components/ui/input.tsx | Auto-generates/derives id and name for <input> with documented resolution rules. |
| src/components/ui/LabeledField.tsx | New compound components that connect labels, descriptions, and errors to inputs/textareas via shared IDs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <Input | ||
| ref={ref} | ||
| id={fieldId} | ||
| name={name} | ||
| aria-describedby={ariaDescribedBy} | ||
| aria-invalid={!!error} | ||
| aria-required={required} | ||
| className={className} | ||
| {...inputProps} | ||
| /> |
| <Textarea | ||
| ref={ref} | ||
| id={fieldId} | ||
| name={name} | ||
| aria-describedby={ariaDescribedBy} | ||
| aria-invalid={!!error} | ||
| aria-required={required} | ||
| className={className} | ||
| {...textareaProps} | ||
| /> |
| const descriptionId = description ? `${fieldId}-description` : undefined; | ||
| const errorId = error ? `${fieldId}-error` : undefined; | ||
|
|
||
| const ariaDescribedBy = | ||
| [descriptionId, errorId].filter(Boolean).join(' ') || undefined; |
| {description && !error && ( | ||
| <p id={descriptionId} className="text-xs text-muted-foreground"> | ||
| {description} | ||
| </p> | ||
| )} | ||
|
|
||
| {error && ( | ||
| <p id={errorId} className="text-xs font-medium text-destructive" role="alert"> | ||
| {error} | ||
| </p> | ||
| )} |
| <Input | ||
| ref={ref} | ||
| id={fieldId} | ||
| name={name} | ||
| aria-describedby={ariaDescribedBy} | ||
| aria-invalid={!!error} | ||
| aria-required={required} | ||
| className={className} | ||
| {...inputProps} | ||
| /> |
| <Textarea | ||
| ref={ref} | ||
| id={fieldId} | ||
| name={name} | ||
| aria-describedby={ariaDescribedBy} | ||
| aria-invalid={!!error} | ||
| aria-required={required} | ||
| className={className} | ||
| {...textareaProps} | ||
| /> |
| * Renders a <Label> + <Input> pair with a shared `id` so the browser | ||
| * correctly associates them. The id is auto-generated when not supplied. | ||
| */ | ||
| export const LabeledField = React.forwardRef<HTMLInputElement, LabeledFieldProps>( |
| /** | ||
| * Renders a <Label> + <Textarea> pair with a shared `id`. | ||
| */ | ||
| export const LabeledTextarea = React.forwardRef< |
🐛 Bugs (DevTools → Aba Problemas)
Bug 1 — 19 resources:
A form field element should have an id or name attributeCampos
<Input>e<Textarea>semidouname→ o navegador não consegue fazer autocomplete corretamente e ferramentas de acessibilidade não identificam o campo.Bug 2 — 6 resources:
No label associated with a form field<Label>semhtmlForapontando para um campo existente → leitores de tela não conseguem associar o rótulo ao input.✅ Correção
Bug 1 — Primitivos com
useId()fallback (zero mudanças nos call sites)input.tsxetextarea.tsxagora resolvemidenameautomaticamente:Exemplos de comportamento:
id="x" name="y""x""y"name="email""email""email"id="my-field""my-field""my-field":r0:(useId):r0:(useId)Zero breaking changes — callers que já passam
id/namenão são afetados.Bug 2 — Novo
LabeledField.tsx(fix arquitetural)Componente compound que auto-vincula
<Label htmlFor>ao<Input id>viauseId()compartilhado.Também exporta
LabeledTextareapara textareas.As 6 labels desconectadas devem ser migradas para
<LabeledField>nos call sites.Arquivos alterados
src/components/ui/input.tsxuseId()fallback paraidenamesrc/components/ui/textarea.tsxsrc/components/ui/LabeledField.tsxLabeledField+LabeledTextareaComo testar
Após deploy, abrir DevTools → Aba Problemas no dashboard:
<LabeledField>Summary by cubic
Auto‑gera id e name nos
InputeTextareae adicionaLabeledField/LabeledTextareapara vincularLabelao campo. Resolve os avisos de acessibilidade no DevTools sobre campos sem id/name e labels sem associação.Bug Fixes
Input/Textarea: resolvemid/nameautomaticamente (id: prop → derivado de name →React.useId(); name: prop → igual ao id). Sem breaking changes.LabeledField.tsx: novo componente que ligaLabel htmlForaoiddo campo comuseIdcompartilhado; expõeLabeledTextareae suportadescription/errorviaaria-describedby.Migration
Label+Input/Textareanão vinculados paraLabeledField/LabeledTextarea.Written for commit 7f64a58. Summary will update on new commits. Review in cubic