Skip to content

feat: add support for nested fields rendered inside card selectable groups#3833

Merged
sauldom102 merged 10 commits intomainfrom
add-support-for-nested-fields-rendered-inside-card-selectable-groups
Apr 3, 2026
Merged

feat: add support for nested fields rendered inside card selectable groups#3833
sauldom102 merged 10 commits intomainfrom
add-support-for-nested-fields-rendered-inside-card-selectable-groups

Conversation

@sauldom102
Copy link
Copy Markdown
Collaborator

Add support for nested fields rendered inside card selectable groups

Screenshot 2026-04-02 at 17 37 32

Problem

F0Form had no way to:

  1. Nest dependent fields inside switch toggle cards — fields conditional on a switch (renderIf: { fieldId, equalsTo: true }) rendered below the switch group as standalone fields, with no visual association to the toggle that controls them.
  2. Use a cardSelect field type — radio-style card selection (single choice among bordered cards) didn't exist as a declarative field type.
  3. Nest dependent fields inside cardSelect options — fields conditional on a specific cardSelect value had no way to expand inline within the selected card.
  4. Opt out of grouping — consecutive switches were always merged into a single bordered container; cardSelect options were always grouped with dividers. There was no way to render them standalone.

Changes

1. selectedContent on CardSelectable (animated expand/collapse)

  • Added selectedContent?: ReactNode prop to CardSelectableItem
  • When an item is selected, its selectedContent expands with a spring animation (motion/react); it collapses when deselected
  • Works for both grouped (divider-separated) and ungrouped (individual bordered cards) layouts

2. Dependent field absorption into switch groups

  • Fields with renderIf: { fieldId: "<switchFieldId>", equalsTo: true } are automatically detected and rendered as selectedContent inside the corresponding switch card
  • Supports both standalone fields and row-grouped fields as nested content
  • isDependentOnSwitch helper performs static analysis of object-form renderIf conditions

3. New cardSelect field type

  • New field type for F0Form: fieldType: "cardSelect" with options: CardSelectOption[]
  • Renders CardSelectableContainer with radio-style single selection
  • Supports hideLabel?: boolean to suppress the field label
  • Supports grouped?: boolean (default true) — when false, renders each option as a separate bordered card
  • Registered in Zod schema layer (f0FormField), field type union, and renderFieldInput

4. Dependent field absorption into cardSelect options

  • Fields with renderIf: { fieldId: "<cardSelectFieldId>", equalsTo: "<optionValue>" } are automatically rendered inside the matching card's selectedContent
  • Uses CardSelectDepsContext (React context with Map<string, ReactNode>) to pass rendered content to CardSelectFieldRenderer
  • Works both standalone and inside switch groups (nested cardSelect inside a switch card that itself has nested deps)

5. grouped: false opt-out for switches and cardSelect

  • Added grouped?: boolean to F0SwitchConfig / F0SwitchField and F0CardSelectConfig / F0CardSelectField
  • Switches with grouped: false render as solo bordered cards, breaking the contiguous group
  • CardSelect with grouped: false passes grouped={false} to CardSelectableContainer
  • Implemented in all three copies of groupContiguousSwitches (F0Form, SectionRenderer, F0FormSection)

6. Number input hint suppression

  • NumberFieldRenderer now passes hint="" to prevent NumberInputInternal from auto-generating constraint hints (e.g. "It should be between 0 and 100") — Zod validation errors handle this instead

@sauldom102 sauldom102 requested a review from a team as a code owner April 2, 2026 15:37
Copilot AI review requested due to automatic review settings April 2, 2026 15:37
@github-actions github-actions bot added feat react Changes affect packages/react labels Apr 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-3833 to install the package

Use pnpm i github:factorialco/f0#07bf8f04390cecebaf16141bb05ed1f34ae71f07 to install this specific commit

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

🔍 Visual review for your branch is published 🔍

Here are the links to:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 45.44% 11135 / 24500
🔵 Statements 44.69% 11480 / 25684
🔵 Functions 37.24% 2506 / 6729
🔵 Branches 37.4% 7324 / 19582
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/components/CardSelectable/CardSelectable.tsx 54.28% 67.12% 60% 55.88% 23-32, 52-63, 126, 158-163, 225
packages/react/src/components/CardSelectable/types.ts 100% 100% 100% 100%
packages/react/src/components/F0Form/F0Form.tsx 79.91% 72.94% 77.96% 80.84% 66-73, 88, 130-133, 146-151, 194-205, 561-565, 575-578, 652, 693-699, 724-725, 730-751, 755, 798, 899
packages/react/src/components/F0Form/f0Schema.ts 77.27% 56.25% 100% 77.27% 690, 708, 726-749
packages/react/src/components/F0Form/groupingUtils.tsx 75.8% 73% 69.23% 77.5% 24, 45, 54, 188, 191-228, 283, 301
packages/react/src/components/F0Form/useSchemaDefinition.ts 96.66% 75% 92.85% 97.29% 115-123, 279-284, 506
packages/react/src/components/F0Form/components/F0FormSection.tsx 68.53% 58.33% 66.66% 68.6% 49-67, 155, 192-193, 196-229, 296-305, 333-341
packages/react/src/components/F0Form/components/SectionRenderer.tsx 72.22% 50% 100% 72.22% 43, 75-82, 112-121
packages/react/src/components/F0Form/components/SwitchGroupRenderer.tsx 93.5% 81.81% 92.59% 93.05% 132-134, 155, 176-177, 199
packages/react/src/components/F0Form/fields/FieldRenderer.tsx 79.48% 80.35% 85.71% 78.37% 27-31, 64-103
packages/react/src/components/F0Form/fields/renderFieldInput.tsx 95.45% 96% 100% 95.45% 225
packages/react/src/components/F0Form/fields/types.ts 100% 100% 100% 100%
packages/react/src/components/F0Form/fields/cardSelect/CardSelectDepsContext.ts 100% 100% 100% 100%
packages/react/src/components/F0Form/fields/cardSelect/CardSelectFieldRenderer.tsx 100% 100% 100% 100%
packages/react/src/components/F0Form/fields/cardSelect/types.ts 100% 100% 100% 100%
packages/react/src/components/F0Form/fields/number/NumberFieldRenderer.tsx 100% 100% 100% 100%
packages/react/src/components/F0Form/fields/switch/types.ts 100% 100% 100% 100%
Generated in workflow #12532 for commit 888cf7c by the Vitest Coverage Report Action

Copy link
Copy Markdown
Contributor

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

This PR extends the F0Form system to support inline nested dependent fields inside selectable “card” UIs (switch-card groups and a new cardSelect field type), improving visual grouping and enabling more compact conditional form layouts.

Changes:

  • Added selectedContent support to CardSelectable items with animated expand/collapse behavior.
  • Introduced a new cardSelect field type (incl. hideLabel and grouped options) and wired it through the schema/config + field renderer pipeline.
  • Implemented “dependent field absorption” so fields with renderIf conditions can be rendered inside the controlling switch/card option instead of as standalone fields.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/react/src/experimental/Forms/CardSelectable/types.ts Adds selectedContent?: ReactNode to card items.
packages/react/src/experimental/Forms/CardSelectable/index.stories.tsx Adds stories demonstrating selectedContent behavior.
packages/react/src/experimental/Forms/CardSelectable/CardSelectable.tsx Implements expandable selectedContent with reduced-motion support.
packages/react/src/experimental/Forms/CardSelectable/tests/CardSelectable.test.tsx Adds test coverage for selectedContent rendering and interaction.
packages/react/src/components/F0Form/useSchemaDefinition.ts Maps grouped for switches and adds cardSelect config-to-field conversion.
packages/react/src/components/F0Form/fields/types.ts Extends field type unions/exports to include cardSelect.
packages/react/src/components/F0Form/fields/switch/types.ts Adds grouped?: boolean to switch config/field types.
packages/react/src/components/F0Form/fields/renderFieldInput.tsx Registers CardSelectFieldRenderer in the field render switch.
packages/react/src/components/F0Form/fields/number/NumberFieldRenderer.tsx Suppresses auto-generated number hints by forcing hint="".
packages/react/src/components/F0Form/fields/FieldRenderer.tsx Hides visible label for cardSelect when hideLabel is set.
packages/react/src/components/F0Form/fields/cardSelect/types.ts Defines cardSelect field/config types and renderIf condition typing.
packages/react/src/components/F0Form/fields/cardSelect/CardSelectFieldRenderer.tsx Renders cardSelect using CardSelectableContainer + deps context.
packages/react/src/components/F0Form/fields/cardSelect/CardSelectDepsContext.ts Adds context to pass option-value → dependent content mappings.
packages/react/src/components/F0Form/f0Schema.ts Adds cardSelect to schema config types and f0FormField overloads.
packages/react/src/components/F0Form/F0Form.tsx Implements switch/cardSelect dependency absorption + context wiring.
packages/react/src/components/F0Form/components/SwitchGroupRenderer.tsx Renders absorbed dependent fields inside switch cards via selectedContent.
packages/react/src/components/F0Form/components/SectionRenderer.tsx Mirrors grouping/absorption behavior for section-based rendering.
packages/react/src/components/F0Form/components/F0FormSection.tsx Updates switch grouping logic for per-section form rendering.
packages/react/src/components/F0Form/tests/F0Form.test.tsx Adds tests for cardSelect, nested dependents, and grouped: false.
packages/react/src/components/F0Form/stories/F0Form.stories.tsx Adds stories demonstrating nested dependent rendering + ungrouped behavior.

Copilot AI review requested due to automatic review settings April 2, 2026 16:14
Copy link
Copy Markdown
Contributor

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 22 out of 22 changed files in this pull request and generated 3 comments.

Comment on lines +166 to +170
className={cn(
"flex cursor-pointer items-center gap-3",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-f1-special-ring",
grouped ? "px-4 py-3" : "p-4"
)}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

CardSelectable replaced the shared focusRing() helper with custom focus-visible:* classes. This diverges from the repo convention (and changes behavior vs focusRing() — e.g. missing ring offset). Prefer using focusRing() (optionally with extra classes) so focus styling stays consistent across components.

Copilot uses AI. Check for mistakes.
Comment on lines 169 to +173
// For checkbox and custom fields, label is handled internally
const showLabel = field.type !== "checkbox" && field.type !== "custom"
const showLabel =
field.type !== "checkbox" &&
field.type !== "custom" &&
!(field.type === "cardSelect" && field.hideLabel)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

FieldRenderer renders a <label htmlFor={field.id}> for cardSelect when hideLabel is false, but the cardSelect control is a custom radiogroup built from <div role="radiogroup"> / <div role="radio"> (not a labelable element). This makes the htmlFor association invalid and label-click won’t focus/activate the control. Consider rendering a non-label element for cardSelect labels (or wiring up aria-labelledby/an actual labelable element) so the label is semantically correct and connected to the radiogroup.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +170
// Wrap cardSelect fields with pre-rendered dependent content via context
if (
f.type === "cardSelect" &&
cardSelectDependentFields?.has(f.id)
) {
const valueMap = cardSelectDependentFields.get(f.id)!
const contentMap = new Map<string, React.ReactNode>()
for (const [equalsTo, deps] of valueMap) {
contentMap.set(
equalsTo,
<div key={equalsTo} className="flex flex-col gap-4">
{deps.map((innerDep) =>
"type" in innerDep && innerDep.type === "row" ? (
<RowRenderer
key={innerDep.fields.map((fd) => fd.id).join("-")}
row={innerDep}
sectionId={sectionId}
/>
) : (
<FieldRenderer
key={(innerDep as F0Field).id}
field={innerDep as F0Field}
sectionId={sectionId}
/>
)
)}
</div>
)
}
return (
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

In SwitchGroupRenderer, the logic that builds the Map<optionValue, ReactNode> for nested cardSelect dependent content duplicates buildCardSelectContentMap() from groupingUtils.tsx (and re-implements the same RowRenderer/FieldRenderer mapping). Reusing the shared helper here would reduce duplication and keep nested rendering consistent between standalone cardSelect and switch-nested cardSelect.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 3, 2026 07:32
Copy link
Copy Markdown
Contributor

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 21 out of 21 changed files in this pull request and generated 2 comments.

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

React is imported as a default but never referenced in this file. With noUnusedLocals enabled, this will fail typecheck/lint; remove the default import (or use a named import like Fragment if needed).

Suggested change
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +151
const valueMap = cardSelectDependentFields.get(f.id)!
const contentMap = new Map<string, React.ReactNode>()
for (const [equalsTo, deps] of valueMap) {
contentMap.set(
equalsTo,
<div key={equalsTo} className="flex flex-col gap-4">
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This cardSelect option→content Map is being built inline, duplicating the same mapping logic handled by buildCardSelectContentMap() in groupingUtils. Reusing the shared helper here would reduce duplication and the risk of the two implementations diverging.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 3, 2026 08:16
Copy link
Copy Markdown
Contributor

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 22 out of 22 changed files in this pull request and generated 3 comments.

return <RadioIndicator checked={selected} />
}

const hasSelectedContent = !!item.selectedContent
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

hasSelectedContent is computed with !!item.selectedContent, which treats valid ReactNode values like 0 or an empty string as “no content” and will skip rendering the expandable area. Prefer a null/undefined check (e.g., item.selectedContent != null) so any valid ReactNode can be rendered when intended.

Suggested change
const hasSelectedContent = !!item.selectedContent
const hasSelectedContent = item.selectedContent != null

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +201
} else if (next.type === "row") {
// Absorb the row if ALL its fields depend on the same switch
const rowParents = next.fields.map((f) =>
isDependentOnSwitch(f, switchIds)
)
const firstParent = rowParents[0]
if (firstParent && rowParents.every((p) => p === firstParent)) {
const existing = dependentFields.get(firstParent) ?? []
existing.push(next)
dependentFields.set(firstParent, existing)
i++
continue
}
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

When absorbing a row as dependent on a switch, any cardSelect fields inside that row are not added to cardSelectIds. As a result, subsequent fields that depend on that cardSelect value won’t be absorbed into the switch group (and won’t render inside the selected card). Consider scanning absorbed rows for cardSelect fields and registering their IDs (similar to the field-level path).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +10
import { describe, expect, it, vi } from "vitest"

import type { CardSelectableItem } from "@/components/CardSelectable/types"

import { CardSelectableContainer } from "@/components/CardSelectable/index"
import { zeroRender as render, screen, userEvent } from "@/testing/test-utils"

const baseItems: CardSelectableItem<string>[] = [
{
value: "a",
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This test file duplicates the same CardSelectable selectedContent coverage that also exists under src/components/CardSelectable/__tests__/…, and this experimental/Forms/CardSelectable folder appears to contain only tests (no component implementation). Consider removing one of the duplicates or relocating the test so there’s a single, canonical suite for @/components/CardSelectable behavior.

Copilot uses AI. Check for mistakes.
@sauldom102 sauldom102 merged commit 5d7e2f8 into main Apr 3, 2026
24 checks passed
@sauldom102 sauldom102 deleted the add-support-for-nested-fields-rendered-inside-card-selectable-groups branch April 3, 2026 08:36
@eliseo-juan eliseo-juan mentioned this pull request Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants