Skip to content

Accordion opens the last item on initial mount instead of the first #861

Description

@Shinyaigeek

Summary

<Accordion> from @openuidev/react-ui opens the last item on initial mount, even for a fully static (non-streaming) response. Expected is the first item open (as the pre-refactor implementation did), or all items closed.

This is the same defect shape as the <Tabs> issue #819: a streaming "follow the newest content" heuristic that doesn't exclude the initial mount.

Reproduction

Standalone repro (published packages only, npm install && npm run dev):
https://github.com/Shinyaigeek/openui-accordion-repro

root = Accordion([i1, i2, i3])
i1 = AccordionItem("a", "Apple", [c1])
i2 = AccordionItem("b", "Banana", [c2])
i3 = AccordionItem("c", "Cherry", [c3])
c1 = TextContent("Apple section content")
c2 = TextContent("Banana section content")
c3 = TextContent("Cherry section content")
  • Expected: the first section (Apple) is open on mount, or all sections start closed.
  • Actual: the last section (Cherry) is open on mount.

Cherry section open on mount

Versions: @openuidev/react-ui@0.13.1, @openuidev/react-lang@0.2.9, @openuidev/react-headless@0.9.3, React 19.

Root cause

In packages/react-ui/src/genui-lib/Accordion/index.tsx, the auto-open logic runs during render and treats any growth in item count as "a new item arrived during streaming":

const [openItem, setOpenItem] = React.useState<string>("");
const userHasInteracted = React.useRef(false);
const prevItemCount = React.useRef(0);

// Auto-open: only when a NEW item arrives during streaming
if (!userHasInteracted.current && items.length > prevItemCount.current) {
  const newest = items[items.length - 1];
  if (newest) setOpenItem(newest.props.value);
}
prevItemCount.current = items.length;

On the very first render prevItemCount.current is 0, so a fully static N-item accordion satisfies items.length > 0 and the branch opens items[items.length - 1] — the last section. The baseline pass is indistinguishable from streaming growth.

The earlier implementation had a mount effect that opened the first item; the refactor in #485 removed it, leaving only the auto-open-newest branch.

Related streaming latch in the same branch

While actually streaming there is a second problem: when a new item appears before its value string has finished streaming, setOpenItem latches a prefix of the value (e.g. "ov" for "overview"). The only re-sync trigger is item-count growth, so the completed value never re-syncs; the latched prefix matches no item and the accordion ends up with no section open until the user clicks. (Items are not filtered for complete values — contrast Tabs, which filters value != null.)

Suggested fix

Mirror the Tabs fix (c2b1e7e for #819):

  • Skip the auto-open pass on the baseline render (e.g. seed prevItemCount from the first non-empty items.length), so a static mount opens the first item (or none) instead of the last.
  • Only auto-open items whose value is complete, and re-sync when a previously-partial value completes, so the streaming auto-open doesn't latch a prefix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions