[accordion] Fix trigger behavior bugs#4833
Conversation
Bundle size
PerformanceTotal duration: 1,045.64 ms -241.37 ms(-18.8%) | Renders: 50 (+0) | Paint: 1,585.34 ms -376.52 ms(-19.2%)
8 tests within noise — details Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
aa4b6a3 to
3ea36ec
Compare
commit: |
3ea36ec to
7ba8b53
Compare
7ba8b53 to
ef1c9dc
Compare
Codex Review (GPT-5.5)Reviewed the full PR 4833 branch against freshly fetched 1. Bugs / Issues1. 🔴
|
ef1c9dc to
588c263
Compare
flaviendelangle
left a comment
There was a problem hiding this comment.
PR #4833 Review — [accordion] Fix trigger behavior bugs
Reviewed the PR with three specialized agents (code-reviewer, pr-test-analyzer, silent-failure-hunter), avoiding the two issues Codex already raised:
??vs||on disabled trigger (already fixed in the current PR)- Missing native event assertion in details test (already added)
Critical Issues
None. The three fixes are architecturally sound. Verified the accordionTriggerRefs lifecycle holds up under add/remove/reorder because getActiveTriggers is bounded by accordionItemRefs.current.length (which CompositeList trims), and reorders work via React's callback-ref cleanup-on-identity-change.
Important Issues (test gaps)
1. Missing dynamic add/remove regression test
The parallel-array invariant between accordionItemRefs and accordionTriggerRefs is the whole reason for the new architecture, but no test mounts/unmounts items and asserts arrow navigation still targets the correct remaining triggers. This is the failure mode the new code is most exposed to.
2. Disabled-root test doesn't cover keyboard navigation paths
AccordionRoot.test.tsx:283-316
The new it.each(['root', 'item']) test only fires click, Space, Enter. A regression that flipped || back to ?? would also let a disabled-root trigger participate in Arrow/Home/End navigation — not currently asserted.
3. Trigger-id restoration only covers custom → undefined
Three transitions are uncovered and travel different code paths:
undefined → custom(effect overrides fallback after mount)'a' → 'b'(effect cleanupsetTriggerId(undefined)races the next effect'ssetTriggerId(idProp)and could leave the fallback briefly visible)- Initial render with a stable custom id (regression guard for the simple case)
Suggestions
4. cancel() not tested in controlled mode
Both new BaseUIChangeEventDetails tests use uncontrolled mode. A controlled-mode cancel() test would lock down the propagation through both branches.
5. multiple={true} × event-details forwarding
AccordionRoot.tsx:84-99 takes a different branch under multiple. The new reason/event.type assertions exist for that branch's onValueChange, but the parallel cancel() assertion is missing.
6. Nested-button navigation test omits horizontal & RTL
AccordionRoot.test.tsx:461-504. Existing convention in this file covers both orientations; the new test only covers vertical.
7. Dev-warning suggestion
getActiveTriggers silently drops items whose <Accordion.Item> exists in accordionItemRefs but whose accordionTriggerRefs[i] is null (e.g. an item rendered without a trigger). Worth a dev-only console.error since today the misuse produces silent focus weirdness with no diagnostic.
8. Test style nit
AccordionRoot.test.tsx:306-309 mixes fireEvent.click(trigger); trigger.focus(); user.keyboard(...). Existing convention is user.pointer({ keys: '[MouseLeft]', target: trigger }) followed by user.keyboard(...). Minor.
Strengths
- Switching from
section.querySelector('[type="button"]')to a dedicated indexed ref array is the right fix; relying on DOM order kept breaking with custom render compositions. - Lazy fallback (
triggerId ?? defaultTriggerId) preserves the labeling invariant acrosssetTriggerId(undefined)cleanups, which was the root of the third bug. - Threading
eventDetailsthroughhandleValueChangecorrectly preserves the cancel signal through both the item → root and the user-callback chains; cancel is checked at both layers. - The
React.useCallbackchoice fortriggerRefis correct —useStableCallbackwould freeze the callback identity and prevent React from running cleanup that clears the previousstate.indexslot during reorders.
588c263 to
b1462a7
Compare
This is part of the Codex component behavior and test coverage sweep. It fixes Accordion behavior/API issues found during review and adds focused regression and adjacent sweep coverage for the affected paths.
Root cause
Accordion had edge cases where its implementation and tests did not fully cover the expected behavior/API contract.
Changes
Original findings addressed
AccordionRoot.tsxcreates a freshREASONS.nonedetails object instead of forwarding the trigger details thatAccordion.Itemreceives from Collapsible, so clicking or keyboard-activating a trigger makesonValueChange(_, details).reasonalways"none"with a synthetic"base-ui"event.AccordionTrigger.tsxusessection.querySelector('[type="button"], [role="button"]'), so it returns the first matching descendant, not necessarily theAccordion.Trigger.Accordion.Iteminitializes a generated fallback, butAccordion.Triggeronly setstriggerIdwhenidPropexists and always clears it on cleanup. Ifidchanges from"custom"toundefined, the fallback is not restored, soAccordion.Panellosesaria-labelledby.