Skip to content

fix: Collapsible content never renders in controlled mode#8777

Open
ofried-acutis wants to merge 2 commits intomakeplane:previewfrom
ofried-acutis:fix/collapsible-toggle
Open

fix: Collapsible content never renders in controlled mode#8777
ofried-acutis wants to merge 2 commits intomakeplane:previewfrom
ofried-acutis:fix/collapsible-toggle

Conversation

@ofried-acutis
Copy link

@ofried-acutis ofried-acutis commented Mar 19, 2026

Summary

The Collapsible component in packages/ui never shows its content when used in controlled mode (isOpen prop). This breaks sub-work items on issue detail pages — clicking the toggle arrow changes the arrow direction but the content never appears.

Root Cause

Two issues in the current implementation:

  1. Disclosure.Button from headlessui has its own internal toggle state that conflicts with the controlled isOpen prop — the internal state and localIsOpen get out of sync
  2. Transition with grid-rows-[0fr]/grid-rows-[1fr] animation classes doesn't produce a visible expand/collapse effect

Fix

Replace Disclosure/Transition from headlessui with a plain div/button and simple conditional rendering ({localIsOpen && ...}).

Steps to Reproduce

  1. Create an issue with sub-work items (sub-issues)
  2. Open the issue detail page
  3. Click the sub-work items toggle arrow
  4. Expected: Sub-work items list appears
  5. Actual: Arrow toggles but content never shows

Test Plan

  • Sub-work items collapsible expands and shows content on click
  • Toggling open/closed works correctly
  • Uncontrolled mode (no isOpen prop) still works
  • No TypeScript errors

Summary by CodeRabbit

  • Refactor
    • Simplified collapsible implementation for improved performance and predictability.
    • Improved state handling so explicit open/closed values are respected.
  • Behavior
    • Panels now mount/unmount immediately (remove animated transitions) for instant expand/collapse.
  • Accessibility
    • Button now exposes expanded state via aria-expanded for better screen reader support.

The Collapsible component uses headlessui's Disclosure and Transition
components, but they prevent content from rendering in controlled mode.
The Disclosure.Button has its own internal toggle state that conflicts
with the controlled isOpen prop, and the Transition grid-rows animation
doesn't produce a visible expand/collapse effect.

This affects sub-work items on issue detail pages: clicking the toggle
arrow changes the arrow direction but the content never appears.

Replace Disclosure/Transition with a plain div/button and simple
conditional rendering.

Steps to reproduce:
1. Create an issue with sub-work items
2. Open the issue detail page
3. Click the sub-work items toggle arrow
4. Content never appears despite arrow toggling
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c9adfe4-f284-480e-a2b0-f3ef695efe1c

📥 Commits

Reviewing files that changed from the base of the PR and between fe1301a and 6dcfe54.

📒 Files selected for processing (2)
  • packages/propel/src/collapsible/collapsible.tsx
  • packages/ui/src/collapsible/collapsible.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui/src/collapsible/collapsible.tsx

📝 Walkthrough

Walkthrough

Replaced Headless UI Disclosure/Transition with plain HTML elements and conditional rendering; adjusted local open-state initialization to explicitly distinguish isOpen={false} from isOpen={undefined}; removed transition animations and simplified panel mounting.

Changes

Cohort / File(s) Summary
UI Collapsible refactor
packages/ui/src/collapsible/collapsible.tsx
Removed Disclosure/Transition; replaced with div/button and conditional panel rendering; changed localIsOpen initialization to isOpen !== undefined ? isOpen : !!defaultOpen; added aria-expanded={localIsOpen}; removed enter/leave animation behavior.
Propel Collapsible state fix
packages/propel/src/collapsible/collapsible.tsx
Adjusted localIsOpen initialization to use explicit controlled/uncontrolled check (controlledIsOpen !== undefined ? controlledIsOpen : !!defaultOpen), so isOpen={false} is respected rather than overwritten.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A button, a panel, tidy and neat,
No extra wrappers, no animated feat,
I hop in the code, make the state behave,
Now open or closed is clear and brave,
A tiny refactor — succinct and sweet. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.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
Title check ✅ Passed The title accurately summarizes the main fix: addressing the bug where collapsible content fails to render in controlled mode.
Description check ✅ Passed The description covers summary, root cause, fix, reproduction steps, and test plan, but lacks explicit 'Type of Change' checkboxes and screenshots as required by the template.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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: 1

🧹 Nitpick comments (1)
packages/ui/src/collapsible/collapsible.tsx (1)

23-23: State initialization fix correctly handles controlled vs uncontrolled modes.

The change from isOpen || defaultOpen ? true : false to isOpen !== undefined ? isOpen : !!defaultOpen properly distinguishes between isOpen={false} (controlled, closed) and isOpen={undefined} (uncontrolled, defers to defaultOpen).

However, the PropEl wrapper at packages/propel/src/collapsible/collapsible.tsx:50 uses controlledIsOpen || defaultOpen || false, which has the same bug pattern—treating controlledIsOpen={false} as falsy and falling back to defaultOpen. Apply the same fix there to ensure consistent behavior across both packages.

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

In `@packages/ui/src/collapsible/collapsible.tsx` at line 23, PropEl's controlled
state fallback incorrectly treats controlledIsOpen={false} as falsy and falls
back to defaultOpen; update the logic in the PropEl wrapper (the variable/prop
named controlledIsOpen) to mirror the other component: use a ternary that checks
controlledIsOpen !== undefined ? controlledIsOpen : !!defaultOpen so a false
explicit prop is honored and undefined falls back to defaultOpen.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/collapsible/collapsible.tsx`:
- Around line 42-49: The button is missing the accessibility attribute
aria-expanded; update the rendered button in the collapsible component to
include aria-expanded={localIsOpen} so assistive tech knows the open/closed
state—modify the JSX for the button element (the one using buttonRef,
className={buttonClassName}, onClick={handleOnClick}, and rendering {title}) to
add aria-expanded based on the localIsOpen state.

---

Nitpick comments:
In `@packages/ui/src/collapsible/collapsible.tsx`:
- Line 23: PropEl's controlled state fallback incorrectly treats
controlledIsOpen={false} as falsy and falls back to defaultOpen; update the
logic in the PropEl wrapper (the variable/prop named controlledIsOpen) to mirror
the other component: use a ternary that checks controlledIsOpen !== undefined ?
controlledIsOpen : !!defaultOpen so a false explicit prop is honored and
undefined falls back to defaultOpen.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4a2fd56-48ff-4459-92e9-74b3b3d58ffc

📥 Commits

Reviewing files that changed from the base of the PR and between 1faf06c and fe1301a.

📒 Files selected for processing (1)
  • packages/ui/src/collapsible/collapsible.tsx

@ofried-acutis
Copy link
Author

ofried-acutis commented Mar 19, 2026 via email

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

Tip

For best results, initiate chat on the files or code changes.

@ofried-acutis — the core fix is solid! Replacing the headlessui Disclosure/Transition with a plain <button>/<div> and conditional rendering directly resolves the root cause of the controlled-mode bug, and the state initialization fix (isOpen !== undefined ? isOpen : !!defaultOpen) correctly handles isOpen={false} vs isOpen={undefined}.

However, there are two items flagged in the review that should be addressed before this is fully ready:


1. ⚠️ Missing aria-expanded (actionable)

Since Disclosure.Button from headlessui automatically managed aria-expanded, removing it means screen readers no longer know whether the collapsible is open or closed. This is a minor but real accessibility regression. The fix is a one-liner:

- <button ref={buttonRef} type="button" className={buttonClassName} onClick={handleOnClick}>
+ <button ref={buttonRef} type="button" className={buttonClassName} onClick={handleOnClick} aria-expanded={localIsOpen}>
    {title}
  </button>

2. 🧹 Same bug in the PropEl wrapper (nitpick)

packages/propel/src/collapsible/collapsible.tsx (line 50) appears to use controlledIsOpen || defaultOpen || false, which has the same issue your fix addresses — controlledIsOpen={false} is treated as falsy and incorrectly falls back to defaultOpen. The same ternary pattern should be applied there:

- controlledIsOpen || defaultOpen || false
+ controlledIsOpen !== undefined ? controlledIsOpen : !!defaultOpen

Summary: ✅ The main fix is correct. Address the two items above (especially aria-expanded for accessibility) and the PR should be good to merge.

@ofried-acutis
Copy link
Author

ofried-acutis commented Mar 19, 2026 via email

- Add aria-expanded={localIsOpen} to button for accessibility
- Fix same controlled state bug in propel collapsible wrapper
  (controlledIsOpen={false} was treated as falsy, falling back
  to defaultOpen)
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