Skip to content

Refactore#2

Open
Omaima33 wants to merge 57 commits intodevelopfrom
refactore
Open

Refactore#2
Omaima33 wants to merge 57 commits intodevelopfrom
refactore

Conversation

@Omaima33
Copy link
Copy Markdown

No description provided.

@Omaima33 Omaima33 requested a review from a team as a code owner March 30, 2026 16:18
Copilot AI review requested due to automatic review settings March 30, 2026 16:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the FormKit UI codebase toward a Component–Hook–Model (CHM) architecture and introduces a framework-free core/ layer, while replacing legacy components/adapters with new field/layout/form implementations and tests.

Changes:

  • Added src/core/ utilities (types, validation, conditionals, grid, i18n, errors) with new unit tests and coverage tooling.
  • Introduced new field/layout/form components (DynamicForm internals, standardized Field* components) and removed legacy components/adapters/errors modules.
  • Added/updated changesets and package scripts/deps for coverage reporting.

Reviewed changes

Copilot reviewed 92 out of 128 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/hooks/tests/useAsyncValidation.test.ts Adds tests for async validation behavior (debounce/abort/error handling).
src/hooks/FormKitContext.ts Introduces shared React context + hook under hooks layer.
src/errors/types.ts Removes legacy error type definitions module.
src/errors/index.ts Removes legacy errors barrel export.
src/errors/errorFormatters.ts Removes legacy error formatting utilities.
src/core/validator.ts Adds framework-free validation helpers (Zod mapping, sync/field validation, emptiness check).
src/core/types.ts Adds framework-free core types/constants (FieldType, values, validation mode, debounce consts).
src/core/schema-helpers.ts Adds helpers to create/merge Zod schemas.
src/core/index.ts Adds barrel exports for the core module.
src/core/i18n.ts Adds framework-free translation types + lookup/interpolation utility.
src/core/grid.ts Adds grid class generation utilities (responsive Tailwind helpers).
src/core/errors.ts Adds framework-free custom error classes.
src/core/conditional.ts Adds conditional visibility evaluation utilities.
src/core/tests/validator.test.ts Adds tests for validator utilities.
src/core/tests/schema-helpers.test.ts Adds tests for schema helpers.
src/core/tests/grid.test.ts Adds tests for grid helpers.
src/core/tests/conditional.test.ts Adds tests for conditional logic.
src/components/layout/index.ts Adds layout barrel exports.
src/components/layout/tests/FormSection.test.tsx Adds tests for FormSection layout + grid behavior.
src/components/layout/FormSection.tsx Adds section rendering with fieldset/legend and grid/colSpan support.
src/components/layout/FormActions.tsx Adds standardized action buttons with i18n + loading state.
src/components/layout/FieldLabel.tsx Adds standardized label/legend component with required indicator + i18n.
src/components/layout/FieldGroup.tsx Adds field grouping component using fieldset/legend.
src/components/layout/FieldError.tsx Adds standardized accessible field error rendering.
src/components/index.ts Reworks component exports into CHM modules and re-exports provider/context hook.
src/components/form/index.ts Adds form module barrel exports.
src/components/form/FormStepper.tsx Adds stepper UI for wizard mode with accessibility attributes.
src/components/form/DynamicFormStep.tsx Adds per-step rendering wrapper for wizard mode.
src/components/fields/index.ts Adds fields module barrel exports.
src/components/fields/tests/TimeField.test.tsx Adds tests for TimeField behavior/a11y.
src/components/fields/tests/TagsField.test.tsx Adds tests for TagsField add/remove/limits behavior.
src/components/fields/tests/SliderField.test.tsx Adds tests for SliderField behavior and a11y attrs.
src/components/fields/tests/RatingField.test.tsx Adds tests for RatingField interactions + keyboard support.
src/components/fields/tests/RangeSliderField.test.tsx Adds tests for RangeSliderField behavior.
src/components/fields/tests/PhoneField.test.tsx Adds tests for PhoneField interactions and structure.
src/components/fields/tests/PasswordField.test.tsx Adds tests for PasswordField toggle + a11y.
src/components/fields/tests/OTPField.test.tsx Adds tests for OTPField input/paste/navigation.
src/components/fields/tests/MultiSelectField.test.tsx Adds tests for MultiSelectField interactions and a11y.
src/components/fields/tests/DateTimeField.test.tsx Adds tests for DateTimeField behavior/a11y.
src/components/fields/TextareaField.tsx Adds new textarea field implementation using FormKit context.
src/components/fields/TextField.tsx Adds new text/email/number input field implementation.
src/components/fields/TagsField.tsx Adds tags input with limits/duplicates/paste support.
src/components/fields/SwitchField.tsx Adds switch field implementation with role="switch".
src/components/fields/SliderField.tsx Adds range slider field with optional numeric input.
src/components/fields/RatingField.tsx Adds rating field with stars, half-step, and keyboard controls.
src/components/fields/RadioGroupField.tsx Adds radio group field using fieldset/legend + radiogroup.
src/components/fields/PasswordField.tsx Adds password field with show/hide toggle and improved styling.
src/components/fields/OTPField.tsx Adds OTP input field with focus management and paste support.
src/components/fields/FileField.tsx Adds file upload field with type/size validation and i18n hints.
src/components/fields/Field.tsx Adds field router (type-based rendering + conditional visibility).
src/components/fields/CheckboxField.tsx Adds checkbox field with custom visuals + hidden native input.
src/components/context/index.ts Adds context module barrel exports.
src/components/context/I18nContext.tsx Adds i18n provider/context with deep-merge support.
src/components/context/FormKitContext.tsx Adds provider component bridging to hooks/FormKitContext.
src/components/tests/ErrorMessage.test.tsx Removes tests for legacy ErrorMessage component.
src/components/Textarea.tsx Removes legacy Textarea component.
src/components/Select.tsx Removes legacy Select component.
src/components/RadioGroup.tsx Removes legacy RadioGroup component.
src/components/Input.tsx Removes legacy Input component.
src/components/FormField.tsx Removes legacy FormField wrapper component.
src/components/ErrorMessage.tsx Removes legacy ErrorMessage component.
src/components/Checkbox.tsx Removes legacy Checkbox component.
src/adapters/zod.ts Removes legacy Zod adapter.
src/adapters/react-hook-form.ts Removes legacy React Hook Form adapter.
src/adapters/index.ts Removes legacy adapters barrel export.
src/adapters/tests/zod.test.ts Removes tests for legacy Zod adapter.
package.json Adds Vitest coverage dependency and test:cov script.
.changeset/instructions-refactor.md Adds changeset describing refactor + testing/coverage claims.
.changeset/improved-field-styles.md Adds changeset describing style improvements.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +61
// Column span class
const colSpanClass = config.colSpan ? `col-span-${config.colSpan}` : '';
const wrapperClass = `formkit-field ${colSpanClass} ${config.className ?? ''}`.trim();

// Route to appropriate field component based on type
const renderField = (): JSX.Element => {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

config.colSpan can be a responsive config object (e.g. { default: 12, md: 6 }), but this code interpolates it into a class string, producing col-span-[object Object]. Also, FormSection already wraps each rendered Field in a div with a col-span class, so this can lead to duplicated/conflicting col-span classes. A concrete fix is to remove col-span handling from Field entirely (let layout components own grid span), or switch to getColSpanClass(config.colSpan) and ensure span is applied in only one place.

Suggested change
// Column span class
const colSpanClass = config.colSpan ? `col-span-${config.colSpan}` : '';
const wrapperClass = `formkit-field ${colSpanClass} ${config.className ?? ''}`.trim();
// Route to appropriate field component based on type
const renderField = (): JSX.Element => {
// Base wrapper class (layout components like FormSection own grid span)
const wrapperClass = `formkit-field ${config.className ?? ''}`.trim();
// Route to appropriate field component based on type
const renderField = (): JSX.Element => {
const renderField = (): JSX.Element => {

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +46
[showError ? errorId : null, config.description ? descId : null].filter(Boolean).join(' ') ||
undefined;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

aria-describedby always includes descId when config.description exists, but the description element is not rendered when showError is true. This causes aria-describedby to reference a non-existent element, which is an accessibility defect. Fix by only including descId when the description is actually rendered (e.g. (!showError && config.description ? descId : null)), or always render the description node (optionally visually hidden) so the reference remains valid. The same pattern appears in other field components in this PR (e.g., OTP/Tags/Slider/Rating) and should be made consistent.

Suggested change
[showError ? errorId : null, config.description ? descId : null].filter(Boolean).join(' ') ||
undefined;
[showError ? errorId : null, !showError && config.description ? descId : null]
.filter(Boolean)
.join(' ') || undefined;

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +150
{config.description && !showError && (
<p id={descId} className="text-xs text-gray-500 mt-1">
{config.description}
</p>
)}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

aria-describedby always includes descId when config.description exists, but the description element is not rendered when showError is true. This causes aria-describedby to reference a non-existent element, which is an accessibility defect. Fix by only including descId when the description is actually rendered (e.g. (!showError && config.description ? descId : null)), or always render the description node (optionally visually hidden) so the reference remains valid. The same pattern appears in other field components in this PR (e.g., OTP/Tags/Slider/Rating) and should be made consistent.

Copilot uses AI. Check for mistakes.
/**
* Field errors mapping (field key → error message)
*/
export type FieldErrors<T extends FormValues = FormValues> = Partial<Record<keyof T, string>>;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

mapZodErrors's generic T is not inferable from the current parameter type (z.ZodError without a type argument), and the implementation produces string paths like 'user.email' / 'items.0' that cannot be represented safely as keyof T. This can cause TypeScript callers/tests to either fail type-checking or rely on unsafe casts. Consider changing the signature to accept z.ZodError<T> (and default T), and loosening FieldErrors to support string paths (e.g. Partial<Record<string, string>>) or splitting root-level vs nested paths explicitly.

Copilot uses AI. Check for mistakes.
* }
* ```
*/
export function mapZodErrors<T extends FormValues>(zodError: z.ZodError): FieldErrors<T> {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

mapZodErrors's generic T is not inferable from the current parameter type (z.ZodError without a type argument), and the implementation produces string paths like 'user.email' / 'items.0' that cannot be represented safely as keyof T. This can cause TypeScript callers/tests to either fail type-checking or rely on unsafe casts. Consider changing the signature to accept z.ZodError<T> (and default T), and loosening FieldErrors to support string paths (e.g. Partial<Record<string, string>>) or splitting root-level vs nested paths explicitly.

Copilot uses AI. Check for mistakes.

for (const issue of zodError.issues) {
// Get the field path (first element for simple fields, joined for nested)
const fieldKey = issue.path.length > 0 ? issue.path.join('.') : '_root';
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

mapZodErrors's generic T is not inferable from the current parameter type (z.ZodError without a type argument), and the implementation produces string paths like 'user.email' / 'items.0' that cannot be represented safely as keyof T. This can cause TypeScript callers/tests to either fail type-checking or rely on unsafe casts. Consider changing the signature to accept z.ZodError<T> (and default T), and loosening FieldErrors to support string paths (e.g. Partial<Record<string, string>>) or splitting root-level vs nested paths explicitly.

Copilot uses AI. Check for mistakes.

// Only keep the first error per field
if (!(fieldKey in errors)) {
errors[fieldKey as keyof T] = issue.message;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

mapZodErrors's generic T is not inferable from the current parameter type (z.ZodError without a type argument), and the implementation produces string paths like 'user.email' / 'items.0' that cannot be represented safely as keyof T. This can cause TypeScript callers/tests to either fail type-checking or rely on unsafe casts. Consider changing the signature to accept z.ZodError<T> (and default T), and loosening FieldErrors to support string paths (e.g. Partial<Record<string, string>>) or splitting root-level vs nested paths explicitly.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,14 @@
---
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The first line contains a BOM/zero-width character before --- (---). Changesets frontmatter parsing can break when the file doesn't start with a literal ---. Remove the BOM so line 1 begins exactly with ---.

Suggested change
---
---

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +42
'useFormKitContext must be used within a DynamicForm. ' +
'Make sure your component is rendered inside <DynamicForm>.',
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The thrown error message refers specifically to DynamicForm, but the JSDoc above says it throws if used outside FormKitProvider. This is confusing for consumers who may use the provider directly (or if the provider component name changes). Align the message with the actual contract (e.g., reference FormKitProvider and optionally mention DynamicForm as a common wrapper).

Suggested change
'useFormKitContext must be used within a DynamicForm. ' +
'Make sure your component is rendered inside <DynamicForm>.',
'useFormKitContext must be used within a FormKitProvider (e.g., via DynamicForm). ' +
'Make sure your component is rendered inside a FormKitProvider or <DynamicForm>.',

Copilot uses AI. Check for mistakes.

return (
<li
key={step.title}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Using step.title as the React key can break list reconciliation if titles are not unique or change (e.g., localized titles). Prefer a stable unique identifier from StepConfig (like step.id if available), or fall back to index only if the order is stable and no better key exists.

Suggested change
key={step.title}
key={index}

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 30, 2026 16:56
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 94 out of 128 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


expect(() => {
renderHook(() => useFormKitContext());
}).toThrow('useFormKitContext must be used within a DynamicForm');
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The expected error message no longer matches the actual message thrown by useFormKitContext (the hook now includes additional guidance text). Update the assertion to match the full message, or assert a stable substring/regex (e.g., toThrow(/useFormKitContext must be used within a DynamicForm/)) to avoid brittle failures.

Suggested change
}).toThrow('useFormKitContext must be used within a DynamicForm');
}).toThrow(/useFormKitContext must be used within a DynamicForm/);

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +40
export function mapZodErrors<T extends FormValues>(zodError: z.ZodError): FieldErrors<T> {
const errors: FieldErrors<T> = {};

for (const issue of zodError.issues) {
// Get the field path (first element for simple fields, joined for nested)
const fieldKey = issue.path.length > 0 ? issue.path.join('.') : '_root';

// Only keep the first error per field
if (!(fieldKey in errors)) {
errors[fieldKey as keyof T] = issue.message;
}
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

mapZodErrors can emit keys like 'user.email', 'items.0', and '_root', but FieldErrors<T> is typed as Partial<Record<keyof T, string>>, which cannot represent these string paths safely. This can mislead API consumers and forces unsafe casting. Consider changing FieldErrors to a Record<string, string> (or a dedicated Path<T> type if you want strongly-typed nested paths) and explicitly include '_root' in the type if supported.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +121
export function mergeSchemas<T extends z.ZodRawShape[]>(schemas: {
[K in keyof T]: z.ZodObject<T[K]>;
}): z.ZodObject<T[number]> {
if (schemas.length === 0) {
return z.object({});
}

let merged = schemas[0];
for (let i = 1; i < schemas.length; i++) {
merged = merged.merge(schemas[i]) as typeof merged;
}

return merged as z.ZodObject<T[number]>;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The generic typing here is unsound in a few ways: (1) T[number] represents a union of shapes, but merge() produces an intersection-like merged shape; (2) for an empty array, returning z.object({}) doesn’t match the declared return type; (3) the parameter type is more complex than needed for callers. A more accurate and ergonomic API is mergeSchemas(schemas: z.AnyZodObject[]): z.AnyZodObject (optionally with overloads), which also cleanly supports mergeSchemas([]) without type-casting.

Suggested change
export function mergeSchemas<T extends z.ZodRawShape[]>(schemas: {
[K in keyof T]: z.ZodObject<T[K]>;
}): z.ZodObject<T[number]> {
if (schemas.length === 0) {
return z.object({});
}
let merged = schemas[0];
for (let i = 1; i < schemas.length; i++) {
merged = merged.merge(schemas[i]) as typeof merged;
}
return merged as z.ZodObject<T[number]>;
export function mergeSchemas(schemas: z.AnyZodObject[]): z.AnyZodObject {
if (schemas.length === 0) {
return z.object({});
}
let merged: z.AnyZodObject = schemas[0];
for (let i = 1; i < schemas.length; i++) {
merged = merged.merge(schemas[i]);
}
return merged;

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +34
<svg className="w-6 h-6 sm:w-8 sm:h-8" viewBox="0 0 24 24" fill="none">
<defs>
<linearGradient id="halfFill">
<stop offset="50%" stopColor="currentColor" />
<stop offset="50%" stopColor="transparent" />
</linearGradient>
</defs>
<path
d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
fill="url(#halfFill)"
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The gradient id="halfFill" is not unique. Rendering multiple RatingField instances on the same page can cause SVG ID collisions, leading to incorrect fills (the url(#halfFill) reference may bind to a different instance). Use a unique ID per component instance (e.g., via useId() or by parameterizing an ID) and reference that in the fill="url(#...)".

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +138
<div
onClick={handleContainerClick}
className={`
formkit-tags-container
flex flex-wrap gap-2 p-2
border rounded-lg
min-h-[42px]
cursor-text
transition-all duration-150
${
showError
? 'border-red-500 focus-within:ring-2 focus-within:ring-red-500'
: 'border-gray-300 hover:border-gray-400 focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500'
}
${isDisabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'}
`}
aria-describedby={describedBy}
>
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

aria-describedby is applied to the container <div>, but the actual focusable control is the <input> inside. Screen readers typically announce descriptions for the focused element, so error/description content may not be read when users tab into the input. Move/add aria-describedby={describedBy} to the input (and keep it in sync with error/description rendering).

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
// Context (internal use only)
export { default as FormKitProvider, useFormKitContext } from './context/FormKitContext';
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

This exports FormKitProvider/useFormKitContext from the components entrypoint while the context file comments state it’s “Internal only — not exported from public API”. If the intent is truly internal, these should not be re-exported from the public components barrel; otherwise, update the internal-only documentation to reflect that these are supported exports.

Suggested change
// Context (internal use only)
export { default as FormKitProvider, useFormKitContext } from './context/FormKitContext';
// Context (internal use only - not exported from public API)

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +58
// Column span class
const colSpanClass = config.colSpan ? `col-span-${config.colSpan}` : '';
const wrapperClass = `formkit-field ${colSpanClass} ${config.className ?? ''}`.trim();
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

FormSection already applies colSpan via getColSpanClass(field.colSpan) and wraps each field, so this duplicates layout responsibility and can also generate invalid classes if config.colSpan is not a simple number (e.g., responsive object). Consider removing colSpan handling from Field entirely (preferred, since layout is handled by FormSection), or use the same getColSpanClass helper consistently and avoid double-wrapping.

Suggested change
// Column span class
const colSpanClass = config.colSpan ? `col-span-${config.colSpan}` : '';
const wrapperClass = `formkit-field ${colSpanClass} ${config.className ?? ''}`.trim();
const wrapperClass = `formkit-field ${config.className ?? ''}`.trim();

Copilot uses AI. Check for mistakes.
@Omaima33 Omaima33 closed this Mar 30, 2026
@Omaima33 Omaima33 reopened this Mar 30, 2026
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
10.1% Duplication on New Code (required ≤ 3%)
B Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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.

2 participants