Skip to content

feat: form components#718

Open
rohanchkrabrty wants to merge 1 commit intomainfrom
feat-form-components
Open

feat: form components#718
rohanchkrabrty wants to merge 1 commit intomainfrom
feat-form-components

Conversation

@rohanchkrabrty
Copy link
Contributor

@rohanchkrabrty rohanchkrabrty commented Mar 23, 2026

Description

This PR adds new Form components

  • Form
  • Field
  • Fieldset

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced Field component for managing labels, helper text, error states, and required/optional indicators
    • Introduced Fieldset component for grouping related form fields
    • Introduced Form component for handling form submissions
    • Simplified InputField and TextArea to work seamlessly with Field wrapper
  • Documentation

    • Added comprehensive guides and interactive examples for Field, Fieldset, and Form components

@rohanchkrabrty rohanchkrabrty requested a review from rsbh March 23, 2026 09:26
@vercel
Copy link

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
apsara Ready Ready Preview, Comment Mar 23, 2026 9:27am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

This PR introduces three new form-related components—Field, Fieldset, and Form—to the raystack library. Field provides label, error, and helper text management as a wrapper component. Fieldset groups related fields semantically. Form wraps native HTML form elements. Concurrently, InputField and TextArea components are refactored to remove label/error responsibility, delegating these concerns to the new Field wrapper. Documentation, tests, demos, and playground examples are included.

Changes

Cohort / File(s) Summary
Field Component Implementation
packages/raystack/components/field/field.tsx, packages/raystack/components/field/field.module.css, packages/raystack/components/field/index.tsx
New Field component with both simple props API (label, helperText, error) and sub-component API (Field.Label, Field.Control, Field.Error, Field.Description). Includes CSS styling for field layout, labels, required/optional indicators, and validation states.
Field Component Tests
packages/raystack/components/field/__tests__/field.test.tsx
Comprehensive test suite covering simple and sub-component APIs, ref forwarding, width behavior, required/optional indicators, error states, helper text, and CSS class application.
Fieldset Component Implementation
packages/raystack/components/fieldset/fieldset.tsx, packages/raystack/components/fieldset/fieldset.module.css, packages/raystack/components/fieldset/index.tsx
New Fieldset component for grouping fields semantically, supporting legend prop and sub-component API (Fieldset.Legend), with CSS for fieldset/legend styling.
Fieldset Component Tests
packages/raystack/components/fieldset/__tests__/fieldset.test.tsx
Test suite validating fieldset rendering, legend API, disabled state, className merging, and ref forwarding.
Form Component Implementation
packages/raystack/components/form/form.tsx, packages/raystack/components/form/form.module.css, packages/raystack/components/form/index.tsx
New Form component wrapping native HTML form element with optional validationMode, errors object, and submission handlers, plus CSS for form layout with vertical spacing.
Form Component Tests
packages/raystack/components/form/__tests__/form.test.tsx
Test suite covering form rendering, className merging, ref forwarding, and submission handling.
Field Component Documentation
apps/www/src/content/docs/components/field/index.mdx, apps/www/src/content/docs/components/field/demo.ts, apps/www/src/content/docs/components/field/props.ts
Complete documentation page with demos (playground, simple, error, helper text, required/optional, sub-component, with various input types) and prop type definitions.
Fieldset Component Documentation
apps/www/src/content/docs/components/fieldset/index.mdx, apps/www/src/content/docs/components/fieldset/demo.ts, apps/www/src/content/docs/components/fieldset/props.ts
Complete documentation page with demos (playground, basic, sub-component, disabled, nested) and prop type definitions.
Form Component Documentation
apps/www/src/content/docs/components/form/index.mdx, apps/www/src/content/docs/components/form/demo.ts, apps/www/src/content/docs/components/form/props.ts
Complete documentation page with demos (playground, basic, multi-field, server errors, validation modes) and prop type definitions.
InputField Refactoring
packages/raystack/components/input-field/input-field.tsx, packages/raystack/components/input-field/input-field.module.css, packages/raystack/components/input-field/__tests__/input-field.test.tsx, apps/www/src/content/docs/components/input-field/...
Removed label, helperText, error, optional, and infoTooltip props from InputField. Deleted label rendering, error wrapper, and helper text markup. Updated CSS to remove label/helper-text classes and apply error styling via data-invalid attribute on wrapper. Updated tests and documentation to reflect Field wrapper usage.
TextArea Refactoring
packages/raystack/components/text-area/text-area.tsx, packages/raystack/components/text-area/text-area.module.css, packages/raystack/components/text-area/__tests__/text-area.test.tsx, apps/www/src/content/docs/components/textarea/...
Removed label, required, infoTooltip, helperText, and error props from TextArea. Eliminated label container, tooltip icon, and error styling. Updated CSS to apply error state via data-invalid attribute. Updated tests and documentation to use Field wrapper for label/error/helper text.
DatePicker Update
packages/raystack/components/calendar/date-picker.tsx
Changed error state signaling from component prop to aria-invalid attribute based on error truthiness.
Playground Examples
apps/www/src/components/playground/field-examples.tsx, apps/www/src/components/playground/fieldset-examples.tsx, apps/www/src/components/playground/form-examples.tsx, apps/www/src/components/playground/input-field-examples.tsx, apps/www/src/components/playground/text-area-examples.tsx, apps/www/src/components/playground/index.ts
New playground example components demonstrating Field, Fieldset, and Form usage. Updated existing InputField and TextArea examples to wrap inputs in Field components for label/error/helper text handling.
Package Exports
packages/raystack/index.tsx
Added exports for new Field, Fieldset, and Form components at package root level.

Sequence Diagram

sequenceDiagram
    actor User
    participant Form
    participant Fieldset
    participant Field
    participant InputField
    participant Validation

    User->>Form: Submit form
    Form->>Validation: Validate fields
    Validation->>Fieldset: Check fieldset state
    Fieldset->>Field: Validate each field
    Field->>InputField: Get input value
    InputField-->>Field: Return value
    Field->>Validation: Run field validator
    Validation-->>Field: Return error/valid
    Field-->>Fieldset: Field result
    Fieldset-->>Form: Fieldset validation result
    Validation-->>Form: All validations complete
    Form-->>User: Display errors or submit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

Do not merge

Suggested reviewers

  • rsbh
  • paanSinghCoder

Poem

🐰 A field of forms blooms in the spring,
With Fieldsets grouped, all validations sing!
No more labels scattered, errors askew,
The new Field wrapper makes forms feel new.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 "feat: form components" accurately summarizes the main change—adding three new form-related components (Form, Field, Fieldset) to the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-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
Contributor

@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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/raystack/components/text-area/text-area.tsx (3)

24-24: ⚠️ Potential issue | 🟡 Minor

style.width is currently overridden by default width.

Line 40 uses { ...style, width }, so with default width='100%' (Line 24), a consumer-provided style.width is ignored.

Suggested fix
-        style={{ ...style, width }}
+        style={{ width, ...style }}

Also applies to: 40-40

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/text-area/text-area.tsx` at line 24, The
component sets a default width prop that ends up overriding any
consumer-provided style.width because the JSX merges props using "{ ...style,
width }"; update the merge so consumer styles win by placing the default width
first and then spreading style (e.g., change the merge in the TextArea component
where style and width are combined to use width before spreading style) or
conditionally apply width only when style.width is not provided; locate this in
the TextArea function/props handling to adjust the object merge order for style
and width.

9-16: ⚠️ Potential issue | 🟠 Major

Breaking prop-removal migration incomplete.

TextArea no longer accepts label in its props interface, but it's still used at apps/www/src/app/examples/page.tsx:1641. This will cause a type error or silently drop the label UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/text-area/text-area.tsx` around lines 9 - 16,
TextAreaProps no longer declares a label prop but callers (e.g., the example
page that passes label to TextArea) still rely on it; either restore the label
prop on the component or remove those usages. To fix, add an optional label?:
React.ReactNode (or string) to the TextAreaProps interface and update the
TextArea component to accept and render that label (e.g., render label above the
textarea when label is provided), or alternatively remove the label prop usage
where TextArea is instantiated. Update references to TextAreaProps and the
TextArea component accordingly so the types and runtime UI stay in sync.

5-16: ⚠️ Potential issue | 🟠 Major

Use TextareaHTMLAttributes to expose textarea-specific props in the type interface.

At line 9, HTMLAttributes<HTMLTextAreaElement> omits textarea-specific props (rows, cols, maxLength, wrap, defaultValue, etc.) from the public type surface. Although the component spreads ...props and accepts these attributes at runtime, the TypeScript interface doesn't reflect that capability. Use React.TextareaHTMLAttributes<HTMLTextAreaElement> (the conventional React type for textarea elements) to provide accurate typing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/text-area/text-area.tsx` around lines 5 - 16,
The TextAreaProps interface currently extends
PropsWithChildren<HTMLAttributes<HTMLTextAreaElement>> which omits
textarea-specific attributes; update it to extend
PropsWithChildren<React.TextareaHTMLAttributes<HTMLTextAreaElement>> so props
like rows, cols, maxLength, wrap, defaultValue, etc. are properly typed; modify
the TextAreaProps declaration (the interface named TextAreaProps) and any import
usage to use React.TextareaHTMLAttributes<HTMLTextAreaElement> instead of
HTMLAttributes<HTMLTextAreaElement>.
packages/raystack/components/calendar/date-picker.tsx (1)

174-189: ⚠️ Potential issue | 🟠 Major

Invalid visual state is not applied after switching to aria-invalid.

Line 178 sets aria-invalid, but InputField invalid styling is keyed off wrapper data-invalid (packages/raystack/components/input-field/input-field.module.css), and the wrapper never receives that attribute (packages/raystack/components/input-field/input-field.tsx). Result: error border/state won’t render for DatePicker.

💡 Proposed fix at root cause (`InputField`)
--- a/packages/raystack/components/input-field/input-field.tsx
+++ b/packages/raystack/components/input-field/input-field.tsx
@@
 export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(
   (
     {
@@
       containerRef,
       ...props
     },
     ref
   ) => {
+    const isInvalid =
+      props['aria-invalid'] === true || props['aria-invalid'] === 'true';
+
     return (
       <div
         className={cx(
           inputWrapper({ size, variant }),
           disabled && styles['input-disabled-wrapper'],
           chips?.length && styles['has-chips']
         )}
+        data-invalid={isInvalid ? '' : undefined}
         style={{ width: width || '100%' }}
         ref={containerRef}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/calendar/date-picker.tsx` around lines 174 -
189, The DatePicker sets aria-invalid on the InputField but InputField's wrapper
uses data-invalid for styling and never receives it, so the invalid visual state
doesn't render; fix by updating the InputField component (input-field.tsx /
InputField) to map/forward the aria-invalid prop to the wrapper as data-invalid
(e.g., compute const isInvalid = props['aria-invalid'] || props.error and add
data-invalid={isInvalid} on the wrapper element) and ensure the InputField prop
types accept aria-invalid so DatePicker's usage continues to work.
🧹 Nitpick comments (11)
apps/www/src/content/docs/components/form/props.ts (1)

19-22: Prefer unknown over any/object in public docs types.

Using Record<string, any> and object weakens type safety in the generated API docs. Consider Record<string, unknown> and Record<string, unknown> (or a named event-details type).

Suggested refinement
   onFormSubmit?: (
-    formValues: Record<string, any>,
-    eventDetails: object
+    formValues: Record<string, unknown>,
+    eventDetails: Record<string, unknown>
   ) => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/form/props.ts` around lines 19 - 22, The
onFormSubmit type weakly uses Record<string, any> and object; change it to use
stronger types such as Record<string, unknown> for formValues and a structured
type for eventDetails (e.g., Record<string, unknown> or a named interface) so
the public docs show precise types — update the onFormSubmit signature to accept
formValues: Record<string, unknown> and eventDetails: Record<string, unknown>
(or replace eventDetails with a named EventDetails type) where the onFormSubmit
declaration is defined.
packages/raystack/index.tsx (1)

36-40: Consider re-exporting the new props types from root as well.

Since ./components/field, ./components/fieldset, and ./components/form expose *Props, adding root type re-exports would make top-level imports consistent for consumers.

Suggested addition
 export { Field } from './components/field';
+export type { FieldProps } from './components/field';
 export { Fieldset } from './components/fieldset';
+export type { FieldsetProps } from './components/fieldset';
 export { Form } from './components/form';
+export type { FormProps } from './components/form';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/index.tsx` around lines 36 - 40, The root re-exports
currently export components Field, Fieldset, FilterChip, Flex, and Form but not
their prop types; add type re-exports for the props (e.g., FieldProps from
'./components/field', FieldsetProps from './components/fieldset', and FormProps
from './components/form') alongside the existing component exports so consumers
can import both components and their types from the package root (keep existing
symbol names: Field, Fieldset, Form and add corresponding FieldProps,
FieldsetProps, FormProps).
packages/raystack/components/text-area/text-area.module.css (1)

62-68: Consolidate disabled styling into one selector.

Line 62 duplicates the same disabled declarations already defined in .textarea:disabled (Line 38+). Keeping both increases drift risk; prefer a single source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/text-area/text-area.module.css` around lines 62
- 68, Duplicate disabled styles exist in the .disabled rule and
.textarea:disabled; remove the .disabled block and consolidate the styles into a
single source of truth by keeping the .textarea:disabled selector (or replace it
with a grouped selector ".textarea:disabled, .disabled" if you must preserve
runtime .disabled class usages), and update any markup that relied on the
.disabled class to use the disabled attribute on the textarea component so
styling remains identical.
apps/www/src/components/playground/form-examples.tsx (1)

24-36: Add name attributes to form controls in the example.

This demo currently omits name, which makes form-value mapping and server-error association less representative of real usage.

♻️ Suggested update
         <Fieldset legend='Personal Information'>
           <Field label='First Name' required>
-            <InputField placeholder='John' />
+            <InputField name='firstName' placeholder='John' />
           </Field>
           <Field label='Last Name' required>
-            <InputField placeholder='Doe' />
+            <InputField name='lastName' placeholder='Doe' />
           </Field>
         </Fieldset>
         <Field label='Email' required helperText="We'll send a confirmation">
-          <InputField type='email' placeholder='john@example.com' />
+          <InputField name='email' type='email' placeholder='john@example.com' />
         </Field>
         <Field label='Message' optional>
-          <TextArea placeholder='Tell us more...' />
+          <TextArea name='message' placeholder='Tell us more...' />
         </Field>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/components/playground/form-examples.tsx` around lines 24 - 36,
The example form controls (InputField and TextArea inside Field/Fieldset) are
missing name attributes; update each form control to include descriptive name
props so form libraries and server errors map correctly — e.g. on the InputField
in the Field labeled "First Name" add name="firstName", the "Last Name"
InputField add name="lastName", the "Email" InputField add name="email", and the
"Message" TextArea add name="message"; keep the existing placeholders, types and
other props intact so only the name prop is introduced on InputField and
TextArea components.
apps/www/src/content/docs/components/textarea/index.mdx (1)

32-34: Make the example snippet self-contained.

error={errors.bio?.message} references an undeclared symbol in this snippet; consider a literal error string or include the surrounding state snippet.

📝 Minimal self-contained snippet option
-<Field label="Bio" helperText="Tell us about yourself" error={errors.bio?.message}>
+<Field label="Bio" helperText="Tell us about yourself" error="Bio is required">
   <TextArea placeholder="Write something..." />
 </Field>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/textarea/index.mdx` around lines 32 -
34, The example uses an undeclared symbol errors.bio?.message which breaks the
snippet; either make the snippet self-contained by replacing
error={errors.bio?.message} with a literal (e.g., error="This field is
required") or include the minimal surrounding form state that declares errors
(for example showing useForm/useState and an errors object) so the Field
component's error prop has a defined source; update the snippet referencing
Field and TextArea accordingly.
packages/raystack/components/fieldset/__tests__/fieldset.test.tsx (1)

68-72: Make the “no legend” assertion implementation-agnostic.

Line 71 currently checks only [role="presentation"], which can miss regressions if legend markup changes. Assert absence of actual legend output instead of a single role selector.

Proposed test hardening
-      expect(
-        container.querySelector('[role="presentation"]')
-      ).not.toBeInTheDocument();
+      expect(container.querySelector('legend')).not.toBeInTheDocument();
+      expect(screen.queryByRole('presentation')).not.toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/fieldset/__tests__/fieldset.test.tsx` around
lines 68 - 72, The test currently checks for a specific role selector
([role="presentation"]) which is implementation-specific; instead update the
"does not render legend when not provided" test to assert absence of an actual
<legend> element (e.g., use container.querySelector('legend') or RTL's
screen.queryByRole/queryByText targeting the legend element) so it fails only if
a legend node is rendered; change the assertion in the Fieldset test to check
for no legend element rather than the role selector.
packages/raystack/components/field/field.module.css (1)

78-81: Strengthen keyboard focus visibility.

Current focus styling relies mainly on border color. Add a :focus-visible ring to improve perceivability for keyboard users.

Suggested CSS tweak
 .input:focus {
   border-color: var(--rs-color-border-accent-emphasis);
   background-color: var(--rs-color-background-base-primary);
 }
+
+.input:focus-visible {
+  outline: 2px solid var(--rs-color-border-accent-emphasis);
+  outline-offset: 1px;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/field/field.module.css` around lines 78 - 81,
The current .input:focus rule only changes border and background which can be
hard to perceive; add a .input:focus-visible rule that applies a clear visible
ring (e.g., an outline or box-shadow using a high-contrast color variable like
--rs-color-border-accent-emphasis or a dedicated --rs-color-focus) while
preserving existing border/background changes so keyboard users get a stronger
visual cue; update the CSS by adding .input:focus-visible { outline: none;
box-shadow: 0 0 0 3px var(--rs-color-focus,
var(--rs-color-border-accent-emphasis)); } (and keep .input:focus as-is) to
target keyboard focus specifically.
apps/www/src/content/docs/components/field/index.mdx (1)

138-140: Prefer behavior-focused accessibility wording.

These lines lock docs to specific attribute internals. Consider phrasing as “automatically associates label/description/error with the control” to reduce future doc drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/field/index.mdx` around lines 138 - 140,
Update the three doc lines to use behavior-focused accessibility wording instead
of specific ARIA attribute names: change the `Field.Label` line to say it
"automatically associates the label with the control", `Field.Description` to
"automatically associates the description with the control", and `Field.Error`
to "automatically associates error messages with the control", referencing the
`Field.Label`, `Field.Description`, and `Field.Error` symbols so the docs
describe behavior rather than specific attributes like
`aria-labelledby`/`aria-describedby`.
packages/raystack/components/field/__tests__/field.test.tsx (1)

30-34: Ref forwarding test is too weak; assert the forwarded element instance.

At Line 33, toHaveBeenCalled() can pass even if only null was passed. Prefer checking ref.current with createRef<HTMLDivElement>().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/field/__tests__/field.test.tsx` around lines 30
- 34, The ref-forwarding test for Field is too weak; replace the mock ref with a
real React ref (e.g., createRef<HTMLDivElement>()) and after render assert the
forwarded element instance instead of just that the ref was called.
Specifically, in the test for Field, use createRef to create ref, render(<Field
ref={ref}>...), then assert ref.current is not null and is the expected element
(e.g., expect(ref.current).toBeInstanceOf(HTMLDivElement) or check tagName) to
verify proper DOM ref forwarding.
apps/www/src/content/docs/components/field/demo.ts (1)

6-15: Avoid forwarding mixed rest props directly to InputField in snippet generation.

At Lines 6–15, rest is blindly serialized into <InputField />. Explicitly separate Field-level props from InputField props to prevent invalid code generation as controls evolve.

✅ Suggested refactor
-export const getCode = (props: any) => {
-  const { label, helperText, error, required, optional, ...rest } = props;
+export const getCode = (props: any) => {
+  const {
+    label,
+    helperText,
+    error,
+    required,
+    optional,
+    width,
+    className,
+    ...inputProps
+  } = props;
   const fieldProps: Record<string, unknown> = {};
   if (label) fieldProps.label = label;
   if (helperText) fieldProps.helperText = helperText;
   if (error) fieldProps.error = error;
   if (required) fieldProps.required = required;
   if (optional) fieldProps.optional = optional;
+  if (width !== undefined) fieldProps.width = width;
+  if (className) fieldProps.className = className;
   return `<Field${getPropsString(fieldProps)}>
-  <InputField${getPropsString(rest)} placeholder="Enter text" />
+  <InputField${getPropsString(inputProps)} placeholder="Enter text" />
 </Field>`;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/field/demo.ts` around lines 6 - 15, The
snippet currently forwards the entire rest object into InputField which can
include Field-level props (label, helperText, error, required, optional) and
produce invalid generated code; update the demo to explicitly separate Field
props from InputField props by building fieldProps from
props.label/helperText/error/required/optional (as now) and building inputProps
by copying rest but removing those Field keys before calling getPropsString; use
getPropsString(fieldProps) for <Field> and getPropsString(inputProps) for
<InputField> so only input-relevant props are serialized.
packages/raystack/components/field/field.tsx (1)

40-40: Use nullish coalescing for width default.

Using || means width={0} or width="" will fallback to '100%'. If a consumer explicitly passes 0, they likely expect 0, not 100%. Prefer ?? for a more predictable default.

♻️ Suggested fix
-        style={{ width: width || '100%' }}
+        style={{ width: width ?? '100%' }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/field/field.tsx` at line 40, The inline style
default for width uses the logical OR operator which treats falsy values like 0
or empty string as absent; update the style expression in the Field component
(the element with style={{ width: width || '100%' }}) to use the nullish
coalescing operator so an explicit 0 or "" is preserved (i.e., change the width
fallback from "width || '100%'" to "width ?? '100%'" in the Field component).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/www/src/components/playground/text-area-examples.tsx`:
- Line 16: The standalone TextArea example renders a control without an
accessible name; update the example that uses the TextArea component so it has
an explicit accessible label by either wrapping it in your Field/Label
components (e.g., add a Field with a Label and put TextArea inside) or by adding
an aria-label (or aria-labelledby) prop directly to TextArea; ensure the label
text is descriptive and not just placeholder content so screen readers can
announce the control.

In `@apps/www/src/content/docs/components/field/props.ts`:
- Around line 29-33: The JSDoc for the validate prop is outdated: update the
comment above the validate function to reflect its actual signature by
mentioning that it may return a string, an array of strings, null, or a Promise
resolving to string | string[] | null; modify the comment line "Custom
validation function. Return an error string or null." to something like "Custom
validation function. Return a string, an array of strings, null, or a Promise
resolving to string | string[] | null." so it matches the validate?: (value:
unknown) => string | string[] | null | Promise<string | string[] | null>;
declaration.

In `@apps/www/src/content/docs/components/fieldset/index.mdx`:
- Around line 60-61: Update the inaccurate description for Fieldset.Legend:
change the text that currently says it "renders a <div> element with
role='presentation'" to state that Fieldset.Legend renders a native <legend>
element (the semantic, accessible element for fieldset legends). Edit the
description near the Fieldset.Legend docs so it matches the implementation and
the later correct note (line 93) that the legend provides the accessible name
for the group.

In `@packages/raystack/components/field/field.tsx`:
- Around line 12-13: The required and optional props on FieldProps/Field can
conflict (both true) causing contradictory UI; make them mutually exclusive or
enforce runtime precedence: update the prop type (FieldProps) to a discriminated
union so required and optional cannot both be provided, or add runtime logic
inside the Field component (e.g., compute const isRequired = props.required ===
true; const isOptional = !isRequired && props.optional === true) and use
isOptional/isRequired when rendering the label so required wins and the optional
marker is suppressed when required is true.

In `@packages/raystack/components/fieldset/__tests__/fieldset.test.tsx`:
- Around line 99-107: The test "sets data-disabled attribute when disabled" only
asserts the data attribute but not native disabled behavior; update this test in
packages/raystack/components/fieldset/__tests__/fieldset.test.tsx (the it block
that renders <Fieldset disabled>...) to also assert that native disabled
semantics are propagated: check that the rendered <fieldset> element has
fieldset.disabled === true and that the child input
(querySelector('[data-testid="child-input"]')) has the disabled attribute (or
disabled property true) and is not focusable/interactable (e.g.,
document.activeElement cannot be the input after a focus attempt). This ensures
both the data attribute and native disabled behavior on Fieldset and its
children are covered.

In `@packages/raystack/components/fieldset/fieldset.tsx`:
- Around line 21-27: The current JSX uses a truthy check `legend &&
<FieldsetPrimitive.Legend ...>` which hides valid ReactNode values like 0;
change this to an explicit null/undefined check so non-null values render.
Locate the render block using FieldsetPrimitive.Legend (and props
legendClassName, styles.legend) in fieldset.tsx and replace the conditional with
a check such as `legend != null` (or `legend !== null && legend !== undefined`)
so numbers and other valid ReactNode values are rendered while still excluding
null/undefined.
- Around line 34-40: The ref type for the FieldsetLegend forwardRef is
incorrect: change the generic from HTMLDivElement to HTMLLegendElement so the
ref matches the rendered <legend> element; update the forwardRef declaration for
FieldsetLegend (and any related type annotations referencing HTMLDivElement) to
HTMLLegendElement while keeping the component body using
FieldsetPrimitive.Legend and cx(styles.legend, className) unchanged.

In `@packages/raystack/components/input-field/input-field.module.css`:
- Around line 28-33: Add an "invalid" boolean prop to InputFieldProps and have
the InputField component set the data-invalid attribute on the wrapper when
invalid is true so the existing .inputWrapper[data-invalid] styles are applied;
update the component's prop destructuring/defaults to include invalid (default
false) and ensure the wrapper element (the one that currently uses className
'inputWrapper') receives data-invalid={invalid} and that the prop is forwarded
where needed and reflected in types/interfaces (InputFieldProps, InputField).

In `@packages/raystack/components/input-field/input-field.tsx`:
- Around line 68-76: The wrapper div for the InputField component (the element
using inputWrapper({ size, variant }) and ref containerRef) never sets the
data-invalid attribute, so the CSS selector .inputWrapper[data-invalid] is never
triggered; update the wrapper rendering in input-field.tsx (both places around
the containerRef usage, including the similar block at lines ~99-113) to add
data-invalid={!!invalid} (or data-invalid when invalid) so the existing
.inputWrapper[data-invalid] styles apply when the component's invalid prop/state
is true.

---

Outside diff comments:
In `@packages/raystack/components/calendar/date-picker.tsx`:
- Around line 174-189: The DatePicker sets aria-invalid on the InputField but
InputField's wrapper uses data-invalid for styling and never receives it, so the
invalid visual state doesn't render; fix by updating the InputField component
(input-field.tsx / InputField) to map/forward the aria-invalid prop to the
wrapper as data-invalid (e.g., compute const isInvalid = props['aria-invalid']
|| props.error and add data-invalid={isInvalid} on the wrapper element) and
ensure the InputField prop types accept aria-invalid so DatePicker's usage
continues to work.

In `@packages/raystack/components/text-area/text-area.tsx`:
- Line 24: The component sets a default width prop that ends up overriding any
consumer-provided style.width because the JSX merges props using "{ ...style,
width }"; update the merge so consumer styles win by placing the default width
first and then spreading style (e.g., change the merge in the TextArea component
where style and width are combined to use width before spreading style) or
conditionally apply width only when style.width is not provided; locate this in
the TextArea function/props handling to adjust the object merge order for style
and width.
- Around line 9-16: TextAreaProps no longer declares a label prop but callers
(e.g., the example page that passes label to TextArea) still rely on it; either
restore the label prop on the component or remove those usages. To fix, add an
optional label?: React.ReactNode (or string) to the TextAreaProps interface and
update the TextArea component to accept and render that label (e.g., render
label above the textarea when label is provided), or alternatively remove the
label prop usage where TextArea is instantiated. Update references to
TextAreaProps and the TextArea component accordingly so the types and runtime UI
stay in sync.
- Around line 5-16: The TextAreaProps interface currently extends
PropsWithChildren<HTMLAttributes<HTMLTextAreaElement>> which omits
textarea-specific attributes; update it to extend
PropsWithChildren<React.TextareaHTMLAttributes<HTMLTextAreaElement>> so props
like rows, cols, maxLength, wrap, defaultValue, etc. are properly typed; modify
the TextAreaProps declaration (the interface named TextAreaProps) and any import
usage to use React.TextareaHTMLAttributes<HTMLTextAreaElement> instead of
HTMLAttributes<HTMLTextAreaElement>.

---

Nitpick comments:
In `@apps/www/src/components/playground/form-examples.tsx`:
- Around line 24-36: The example form controls (InputField and TextArea inside
Field/Fieldset) are missing name attributes; update each form control to include
descriptive name props so form libraries and server errors map correctly — e.g.
on the InputField in the Field labeled "First Name" add name="firstName", the
"Last Name" InputField add name="lastName", the "Email" InputField add
name="email", and the "Message" TextArea add name="message"; keep the existing
placeholders, types and other props intact so only the name prop is introduced
on InputField and TextArea components.

In `@apps/www/src/content/docs/components/field/demo.ts`:
- Around line 6-15: The snippet currently forwards the entire rest object into
InputField which can include Field-level props (label, helperText, error,
required, optional) and produce invalid generated code; update the demo to
explicitly separate Field props from InputField props by building fieldProps
from props.label/helperText/error/required/optional (as now) and building
inputProps by copying rest but removing those Field keys before calling
getPropsString; use getPropsString(fieldProps) for <Field> and
getPropsString(inputProps) for <InputField> so only input-relevant props are
serialized.

In `@apps/www/src/content/docs/components/field/index.mdx`:
- Around line 138-140: Update the three doc lines to use behavior-focused
accessibility wording instead of specific ARIA attribute names: change the
`Field.Label` line to say it "automatically associates the label with the
control", `Field.Description` to "automatically associates the description with
the control", and `Field.Error` to "automatically associates error messages with
the control", referencing the `Field.Label`, `Field.Description`, and
`Field.Error` symbols so the docs describe behavior rather than specific
attributes like `aria-labelledby`/`aria-describedby`.

In `@apps/www/src/content/docs/components/form/props.ts`:
- Around line 19-22: The onFormSubmit type weakly uses Record<string, any> and
object; change it to use stronger types such as Record<string, unknown> for
formValues and a structured type for eventDetails (e.g., Record<string, unknown>
or a named interface) so the public docs show precise types — update the
onFormSubmit signature to accept formValues: Record<string, unknown> and
eventDetails: Record<string, unknown> (or replace eventDetails with a named
EventDetails type) where the onFormSubmit declaration is defined.

In `@apps/www/src/content/docs/components/textarea/index.mdx`:
- Around line 32-34: The example uses an undeclared symbol errors.bio?.message
which breaks the snippet; either make the snippet self-contained by replacing
error={errors.bio?.message} with a literal (e.g., error="This field is
required") or include the minimal surrounding form state that declares errors
(for example showing useForm/useState and an errors object) so the Field
component's error prop has a defined source; update the snippet referencing
Field and TextArea accordingly.

In `@packages/raystack/components/field/__tests__/field.test.tsx`:
- Around line 30-34: The ref-forwarding test for Field is too weak; replace the
mock ref with a real React ref (e.g., createRef<HTMLDivElement>()) and after
render assert the forwarded element instance instead of just that the ref was
called. Specifically, in the test for Field, use createRef to create ref,
render(<Field ref={ref}>...), then assert ref.current is not null and is the
expected element (e.g., expect(ref.current).toBeInstanceOf(HTMLDivElement) or
check tagName) to verify proper DOM ref forwarding.

In `@packages/raystack/components/field/field.module.css`:
- Around line 78-81: The current .input:focus rule only changes border and
background which can be hard to perceive; add a .input:focus-visible rule that
applies a clear visible ring (e.g., an outline or box-shadow using a
high-contrast color variable like --rs-color-border-accent-emphasis or a
dedicated --rs-color-focus) while preserving existing border/background changes
so keyboard users get a stronger visual cue; update the CSS by adding
.input:focus-visible { outline: none; box-shadow: 0 0 0 3px
var(--rs-color-focus, var(--rs-color-border-accent-emphasis)); } (and keep
.input:focus as-is) to target keyboard focus specifically.

In `@packages/raystack/components/field/field.tsx`:
- Line 40: The inline style default for width uses the logical OR operator which
treats falsy values like 0 or empty string as absent; update the style
expression in the Field component (the element with style={{ width: width ||
'100%' }}) to use the nullish coalescing operator so an explicit 0 or "" is
preserved (i.e., change the width fallback from "width || '100%'" to "width ??
'100%'" in the Field component).

In `@packages/raystack/components/fieldset/__tests__/fieldset.test.tsx`:
- Around line 68-72: The test currently checks for a specific role selector
([role="presentation"]) which is implementation-specific; instead update the
"does not render legend when not provided" test to assert absence of an actual
<legend> element (e.g., use container.querySelector('legend') or RTL's
screen.queryByRole/queryByText targeting the legend element) so it fails only if
a legend node is rendered; change the assertion in the Fieldset test to check
for no legend element rather than the role selector.

In `@packages/raystack/components/text-area/text-area.module.css`:
- Around line 62-68: Duplicate disabled styles exist in the .disabled rule and
.textarea:disabled; remove the .disabled block and consolidate the styles into a
single source of truth by keeping the .textarea:disabled selector (or replace it
with a grouped selector ".textarea:disabled, .disabled" if you must preserve
runtime .disabled class usages), and update any markup that relied on the
.disabled class to use the disabled attribute on the textarea component so
styling remains identical.

In `@packages/raystack/index.tsx`:
- Around line 36-40: The root re-exports currently export components Field,
Fieldset, FilterChip, Flex, and Form but not their prop types; add type
re-exports for the props (e.g., FieldProps from './components/field',
FieldsetProps from './components/fieldset', and FormProps from
'./components/form') alongside the existing component exports so consumers can
import both components and their types from the package root (keep existing
symbol names: Field, Fieldset, Form and add corresponding FieldProps,
FieldsetProps, FormProps).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f6a4a6e4-c7b2-4371-b489-cae600196284

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4cf92 and 9ef4c38.

📒 Files selected for processing (41)
  • apps/www/src/components/playground/field-examples.tsx
  • apps/www/src/components/playground/fieldset-examples.tsx
  • apps/www/src/components/playground/form-examples.tsx
  • apps/www/src/components/playground/index.ts
  • apps/www/src/components/playground/input-field-examples.tsx
  • apps/www/src/components/playground/text-area-examples.tsx
  • apps/www/src/content/docs/components/field/demo.ts
  • apps/www/src/content/docs/components/field/index.mdx
  • apps/www/src/content/docs/components/field/props.ts
  • apps/www/src/content/docs/components/fieldset/demo.ts
  • apps/www/src/content/docs/components/fieldset/index.mdx
  • apps/www/src/content/docs/components/fieldset/props.ts
  • apps/www/src/content/docs/components/form/demo.ts
  • apps/www/src/content/docs/components/form/index.mdx
  • apps/www/src/content/docs/components/form/props.ts
  • apps/www/src/content/docs/components/input-field/demo.ts
  • apps/www/src/content/docs/components/input-field/index.mdx
  • apps/www/src/content/docs/components/input-field/props.ts
  • apps/www/src/content/docs/components/textarea/demo.ts
  • apps/www/src/content/docs/components/textarea/index.mdx
  • apps/www/src/content/docs/components/textarea/props.ts
  • packages/raystack/components/calendar/date-picker.tsx
  • packages/raystack/components/field/__tests__/field.test.tsx
  • packages/raystack/components/field/field.module.css
  • packages/raystack/components/field/field.tsx
  • packages/raystack/components/field/index.tsx
  • packages/raystack/components/fieldset/__tests__/fieldset.test.tsx
  • packages/raystack/components/fieldset/fieldset.module.css
  • packages/raystack/components/fieldset/fieldset.tsx
  • packages/raystack/components/fieldset/index.tsx
  • packages/raystack/components/form/__tests__/form.test.tsx
  • packages/raystack/components/form/form.module.css
  • packages/raystack/components/form/form.tsx
  • packages/raystack/components/form/index.tsx
  • packages/raystack/components/input-field/__tests__/input-field.test.tsx
  • packages/raystack/components/input-field/input-field.module.css
  • packages/raystack/components/input-field/input-field.tsx
  • packages/raystack/components/text-area/__tests__/text-area.test.tsx
  • packages/raystack/components/text-area/text-area.module.css
  • packages/raystack/components/text-area/text-area.tsx
  • packages/raystack/index.tsx

<Field label='Error TextArea' error='This field has an error'>
<TextArea placeholder='Enter your text here' />
</Field>
<TextArea placeholder='Without Field wrapper' width='300px' />
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Standalone TextArea example is missing an accessible name.

Line 16 renders a control without Field label or aria-label. Placeholder text is not a reliable accessible label.

✅ Suggested fix
-        <TextArea placeholder='Without Field wrapper' width='300px' />
+        <TextArea
+          placeholder='Without Field wrapper'
+          aria-label='Without Field wrapper'
+          width='300px'
+        />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TextArea placeholder='Without Field wrapper' width='300px' />
<TextArea
placeholder='Without Field wrapper'
aria-label='Without Field wrapper'
width='300px'
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/components/playground/text-area-examples.tsx` at line 16, The
standalone TextArea example renders a control without an accessible name; update
the example that uses the TextArea component so it has an explicit accessible
label by either wrapping it in your Field/Label components (e.g., add a Field
with a Label and put TextArea inside) or by adding an aria-label (or
aria-labelledby) prop directly to TextArea; ensure the label text is descriptive
and not just placeholder content so screen readers can announce the control.

Comment on lines +29 to +33
/** Custom validation function. Return an error string or null. */
validate?: (
value: unknown
) => string | string[] | null | Promise<string | string[] | null>;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

validate JSDoc is out of sync with its declared return type.

At Line 29, the comment says “error string or null”, but Lines 30–32 also allow string[] and Promise. Please align docs text with the actual signature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/field/props.ts` around lines 29 - 33,
The JSDoc for the validate prop is outdated: update the comment above the
validate function to reflect its actual signature by mentioning that it may
return a string, an array of strings, null, or a Promise resolving to string |
string[] | null; modify the comment line "Custom validation function. Return an
error string or null." to something like "Custom validation function. Return a
string, an array of strings, null, or a Promise resolving to string | string[] |
null." so it matches the validate?: (value: unknown) => string | string[] | null
| Promise<string | string[] | null>; declaration.

Comment on lines +60 to +61
An accessible legend for the fieldset. Renders a `<div>` element with `role="presentation"`.

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify what Fieldset.Legend renders and how accessible naming is wired.
fd 'fieldset\.tsx$' | while read -r f; do
  echo "=== $f ==="
  rg -n -C3 'Fieldset\.Legend|legend|role=.presentation.|aria-labelledby|<legend' "$f"
done

Repository: raystack/apsara

Length of output: 1144


🏁 Script executed:

# Read the documentation file to see lines 60-61 and 93-94 in context
cat -n apps/www/src/content/docs/components/fieldset/index.mdx | sed -n '50,105p'

Repository: raystack/apsara

Length of output: 1342


🏁 Script executed:

# Find the FieldsetPrimitive import to understand the source
rg -n 'FieldsetPrimitive|from.*fieldset' packages/raystack/components/fieldset/

Repository: raystack/apsara

Length of output: 1326


🏁 Script executed:

# Search for aria-labelledby or accessible naming patterns in fieldset implementation
rg -n 'aria-labelledby|aria-label|aria-describedby|labelled|named' packages/raystack/components/fieldset/

Repository: raystack/apsara

Length of output: 41


🏁 Script executed:

# Check Base UI's Fieldset.Legend rendering behavior
rg -n 'role=.presentation' packages/raystack/components/fieldset/ -A2 -B2

Repository: raystack/apsara

Length of output: 581


🏁 Script executed:

# Search for any CSS or styling that might affect the rendered element
cat packages/raystack/components/fieldset/fieldset.module.css

Repository: raystack/apsara

Length of output: 475


🌐 Web query:

Base UI @base-ui/react/fieldset Legend component rendered element

💡 Result:

The Legend component from @base-ui/react/fieldset renders a <legend> HTML element.

Citations:


Fix inaccurate element description on line 60.

Line 60 incorrectly states that Fieldset.Legend renders a <div> element with role="presentation". Base UI's Legend component actually renders a native <legend> element, which is the correct semantic element for fieldset legends and provides the accessible name for the group (confirming line 93 is accurate). Update line 60 to reflect the correct rendered element.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/www/src/content/docs/components/fieldset/index.mdx` around lines 60 -
61, Update the inaccurate description for Fieldset.Legend: change the text that
currently says it "renders a <div> element with role='presentation'" to state
that Fieldset.Legend renders a native <legend> element (the semantic, accessible
element for fieldset legends). Edit the description near the Fieldset.Legend
docs so it matches the implementation and the later correct note (line 93) that
the legend provides the accessible name for the group.

Comment on lines +12 to +13
required?: boolean;
optional?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

required and optional props can conflict.

Both props can be true simultaneously, causing the label to display both * and (optional) text—a contradictory UX. Consider making them mutually exclusive or having one take precedence.

🛡️ Option: Add runtime precedence (required wins)
         {label && (
           <FieldPrimitive.Label className={styles.label}>
             {label}
             {required && (
               <span className={styles['required-indicator']} aria-hidden='true'>
                 *
               </span>
             )}
-            {optional && <span className={styles.optional}>(optional)</span>}
+            {optional && !required && <span className={styles.optional}>(optional)</span>}
           </FieldPrimitive.Label>
         )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/field/field.tsx` around lines 12 - 13, The
required and optional props on FieldProps/Field can conflict (both true) causing
contradictory UI; make them mutually exclusive or enforce runtime precedence:
update the prop type (FieldProps) to a discriminated union so required and
optional cannot both be provided, or add runtime logic inside the Field
component (e.g., compute const isRequired = props.required === true; const
isOptional = !isRequired && props.optional === true) and use
isOptional/isRequired when rendering the label so required wins and the optional
marker is suppressed when required is true.

Comment on lines +99 to +107
it('sets data-disabled attribute when disabled', () => {
const { container } = render(
<Fieldset disabled>
<input data-testid='child-input' />
</Fieldset>
);
const fieldset = container.querySelector('fieldset');
expect(fieldset).toHaveAttribute('data-disabled', '');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cover native disabled semantics, not only data attributes.

Line 106 verifies styling state (data-disabled) but not behavior. Add assertions for native disabled propagation so this contract can’t regress silently.

Suggested coverage addition
       const fieldset = container.querySelector('fieldset');
       expect(fieldset).toHaveAttribute('data-disabled', '');
+      expect(fieldset).toHaveAttribute('disabled');
+      expect(screen.getByTestId('child-input')).toBeDisabled();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/fieldset/__tests__/fieldset.test.tsx` around
lines 99 - 107, The test "sets data-disabled attribute when disabled" only
asserts the data attribute but not native disabled behavior; update this test in
packages/raystack/components/fieldset/__tests__/fieldset.test.tsx (the it block
that renders <Fieldset disabled>...) to also assert that native disabled
semantics are propagated: check that the rendered <fieldset> element has
fieldset.disabled === true and that the child input
(querySelector('[data-testid="child-input"]')) has the disabled attribute (or
disabled property true) and is not focusable/interactable (e.g.,
document.activeElement cannot be the input after a focus attempt). This ensures
both the data attribute and native disabled behavior on Fieldset and its
children are covered.

Comment on lines +21 to +27
{legend && (
<FieldsetPrimitive.Legend
className={cx(styles.legend, legendClassName)}
>
{legend}
</FieldsetPrimitive.Legend>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Render legend for non-null values instead of truthy-only checks.

At Line 21, legend && suppresses valid ReactNode values like 0. Use a null/undefined check instead.

✅ Suggested fix
-      {legend && (
+      {legend !== undefined && legend !== null && (
         <FieldsetPrimitive.Legend
           className={cx(styles.legend, legendClassName)}
         >
           {legend}
         </FieldsetPrimitive.Legend>
       )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{legend && (
<FieldsetPrimitive.Legend
className={cx(styles.legend, legendClassName)}
>
{legend}
</FieldsetPrimitive.Legend>
)}
{legend !== undefined && legend !== null && (
<FieldsetPrimitive.Legend
className={cx(styles.legend, legendClassName)}
>
{legend}
</FieldsetPrimitive.Legend>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/fieldset/fieldset.tsx` around lines 21 - 27, The
current JSX uses a truthy check `legend && <FieldsetPrimitive.Legend ...>` which
hides valid ReactNode values like 0; change this to an explicit null/undefined
check so non-null values render. Locate the render block using
FieldsetPrimitive.Legend (and props legendClassName, styles.legend) in
fieldset.tsx and replace the conditional with a check such as `legend != null`
(or `legend !== null && legend !== undefined`) so numbers and other valid
ReactNode values are rendered while still excluding null/undefined.

Comment on lines +34 to +40
const FieldsetLegend = forwardRef<
HTMLDivElement,
FieldsetPrimitive.Legend.Props
>(({ className, ...props }, ref) => (
<FieldsetPrimitive.Legend
ref={ref}
className={cx(styles.legend, className)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In @base-ui/react/fieldset, what DOM element type does Fieldset.Legend render, and what ref type should be used with React.forwardRef for it?

💡 Result:

In @base-ui/react/fieldset, Fieldset.Legend renders a <legend> DOM element. For React.forwardRef, use React.RefObject<HTMLLegendElement> or React.Ref<HTMLLegendElement> (equivalent to React.LegacyRef<HTMLLegendElement>), as it is a standard HTML element ref.

Citations:


Change ref type from HTMLDivElement to HTMLLegendElement.

Fieldset.Legend renders a <legend> element, not a div. The ref type must be HTMLLegendElement to correctly reflect the underlying DOM element.

Fix
 const FieldsetLegend = forwardRef<
-  HTMLDivElement,
+  HTMLLegendElement,
   FieldsetPrimitive.Legend.Props
 >(({ className, ...props }, ref) => (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const FieldsetLegend = forwardRef<
HTMLDivElement,
FieldsetPrimitive.Legend.Props
>(({ className, ...props }, ref) => (
<FieldsetPrimitive.Legend
ref={ref}
className={cx(styles.legend, className)}
const FieldsetLegend = forwardRef<
HTMLLegendElement,
FieldsetPrimitive.Legend.Props
>(({ className, ...props }, ref) => (
<FieldsetPrimitive.Legend
ref={ref}
className={cx(styles.legend, className)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/fieldset/fieldset.tsx` around lines 34 - 40, The
ref type for the FieldsetLegend forwardRef is incorrect: change the generic from
HTMLDivElement to HTMLLegendElement so the ref matches the rendered <legend>
element; update the forwardRef declaration for FieldsetLegend (and any related
type annotations referencing HTMLDivElement) to HTMLLegendElement while keeping
the component body using FieldsetPrimitive.Legend and cx(styles.legend,
className) unchanged.

Comment on lines +28 to +33
.inputWrapper[data-invalid] {
border-color: var(--rs-color-border-danger-primary);
}

.input-error-wrapper:hover {
border-color: var(--rs-color-border-danger-emphasis-hover);
}

.input-error-wrapper:focus-within {
.inputWrapper[data-invalid]:hover,
.inputWrapper[data-invalid]:focus-within {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Check Field composition:"
fd '^field\.tsx$' packages | while read -r f; do
  echo "--- $f"
  rg -n "FieldPrimitive\.(Root|Label|Control|Error|Description)|className=\{styles\.control\}" "$f"
done

echo
echo "Check InputField state-attribute wiring:"
fd '^input-field\.tsx$' packages | while read -r f; do
  echo "--- $f"
  rg -n "data-invalid|data-disabled|FieldPrimitive\.Control|aria-invalid|invalid" "$f"
done

Repository: raystack/apsara

Length of output: 1145


🏁 Script executed:

# Check if input-field.tsx exists and read its content
fd '^input-field\.tsx$' packages -x cat {} -n

# Also check the full field.tsx to understand the complete structure
echo "=== Full field.tsx context ==="
cat -n packages/raystack/components/field/field.tsx

# Check what's in input-field directory
echo "=== InputField module files ==="
find packages/raystack/components/input-field -type f | head -20

Repository: raystack/apsara

Length of output: 8457


🏁 Script executed:

# Search for InputField usage patterns
echo "=== InputField usage patterns ==="
rg -n "InputField|data-invalid" packages/raystack/components --type tsx --type ts -B 2 -A 2 | head -100

# Check if InputField should have an invalid prop based on tests
echo "=== InputField test file ==="
cat packages/raystack/components/input-field/__tests__/input-field.test.tsx

Repository: raystack/apsara

Length of output: 9828


🏁 Script executed:

# Read the CSS file to see what [data-invalid] styles exist
echo "=== input-field.module.css content ==="
cat packages/raystack/components/input-field/input-field.module.css

# Search for any usage of InputField in combination with Field
echo -e "\n=== Searching for InputField usage context ==="
rg -r "InputField|Field" packages/raystack/components --type tsx -B 3 -A 3 | grep -A 5 -B 5 "InputField" | head -80

Repository: raystack/apsara

Length of output: 5037


🏁 Script executed:

# Search more broadly for InputField integration with Field
echo "=== Search for Field/InputField combinations ==="
rg "Field|InputField" packages/raystack/components --type ts --type jsx -l | head -20

# Check if there are any examples or stories
find packages -name "*.stories.*" -o -name "*.example.*" | head -10

# Look at the Field module to see exported exports and composition
echo -e "\n=== Check field index exports ==="
cat packages/raystack/components/field/index.tsx 2>/dev/null || echo "No index.tsx found"

Repository: raystack/apsara

Length of output: 263


[data-invalid] selector in InputField CSS is orphaned—component never sets this attribute.

InputField lacks an invalid prop to activate the defined .inputWrapper[data-invalid] styles (lines 28–33 in input-field.module.css). The component accepts disabled and applies the corresponding class conditionally, but provides no API for invalid state.

Add an invalid prop to InputFieldProps and set data-invalid on the wrapper:

Suggested fix
export interface InputFieldProps
  extends Omit<ComponentPropsWithoutRef<'input'>, 'size'>,
    VariantProps<typeof inputWrapper> {
  disabled?: boolean;
+  invalid?: boolean;
  leadingIcon?: ReactNode;
export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(
  (
    {
      className,
      disabled,
+      invalid,
      placeholder,
      <div
        className={cx(
          inputWrapper({ size, variant }),
          disabled && styles['input-disabled-wrapper'],
          chips?.length && styles['has-chips']
        )}
+        data-invalid={invalid || undefined}
        style={{ width: width || '100%' }}
        ref={containerRef}
      >

Note: Field.tsx using a plain div instead of FieldPrimitive.Control is a separate concern and does not affect InputField, which is a standalone component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/input-field/input-field.module.css` around lines
28 - 33, Add an "invalid" boolean prop to InputFieldProps and have the
InputField component set the data-invalid attribute on the wrapper when invalid
is true so the existing .inputWrapper[data-invalid] styles are applied; update
the component's prop destructuring/defaults to include invalid (default false)
and ensure the wrapper element (the one that currently uses className
'inputWrapper') receives data-invalid={invalid} and that the prop is forwarded
where needed and reflected in types/interfaces (InputFieldProps, InputField).

Comment on lines +68 to +76
<div
className={cx(
inputWrapper({ size, variant }),
disabled && styles['input-disabled-wrapper'],
chips?.length && styles['has-chips']
)}
<div
className={cx(
inputWrapper({ size, variant, className }),
error && styles['input-error-wrapper'],
disabled && styles['input-disabled-wrapper'],
chips?.length && styles['has-chips']
)}
ref={containerRef}
>
{leadingIcon && (
<div className={styles['leading-icon']}>{leadingIcon}</div>
)}
{prefix && <div className={styles.prefix}>{prefix}</div>}

<div className={styles['chip-input-container']}>
{chips?.slice(0, maxChipsVisible).map((chip, index) => (
<Chip
key={index}
variant='outline'
isDismissible={!!chip.onRemove}
onDismiss={chip.onRemove}
className={styles.chip}
>
{chip.label}
</Chip>
))}
{chips && chips.length > maxChipsVisible && (
<span className={styles['chip-overflow']}>
+{chips.length - maxChipsVisible}
</span>
)}
<input
ref={ref}
className={cx(
styles['input-field'],
leadingIcon && styles['has-leading-icon'],
trailingIcon && styles['has-trailing-icon'],
prefix && styles['has-prefix'],
suffix && styles['has-suffix'],
error && styles['input-error'],
disabled && styles['input-disabled'],
className
)}
aria-invalid={!!error}
placeholder={placeholder}
disabled={disabled}
{...props}
/>
</div>
style={{ width: width || '100%' }}
ref={containerRef}
>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Invalid border styles are no longer reachable.

packages/raystack/components/input-field/input-field.module.css:28-35 styles .inputWrapper[data-invalid], but Lines 68–76 never set data-invalid on the wrapper. This drops visible error-state styling.

✅ Suggested fix
   (
     {
       className,
       disabled,
+      'aria-invalid': ariaInvalid,
       placeholder,
       leadingIcon,
       trailingIcon,
@@
       ...props
     },
     ref
   ) => {
+    const isInvalid = ariaInvalid === true || ariaInvalid === 'true';
     return (
       <div
@@
         style={{ width: width || '100%' }}
         ref={containerRef}
+        data-invalid={isInvalid ? '' : undefined}
       >
@@
           <input
             ref={ref}
@@
             placeholder={placeholder}
             disabled={disabled}
+            aria-invalid={ariaInvalid}
             {...props}
           />

Also applies to: 99-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/raystack/components/input-field/input-field.tsx` around lines 68 -
76, The wrapper div for the InputField component (the element using
inputWrapper({ size, variant }) and ref containerRef) never sets the
data-invalid attribute, so the CSS selector .inputWrapper[data-invalid] is never
triggered; update the wrapper rendering in input-field.tsx (both places around
the containerRef usage, including the similar block at lines ~99-113) to add
data-invalid={!!invalid} (or data-invalid when invalid) so the existing
.inputWrapper[data-invalid] styles apply when the component's invalid prop/state
is true.

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.

1 participant