Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chain-of-thought-aria-controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ai-elements": patch
---

Fix ChainOfThought header aria-controls pointing at an element that is never rendered
20 changes: 20 additions & 0 deletions packages/elements/__tests__/chain-of-thought.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ describe("chainOfThought", () => {

expect(onOpenChange).toHaveBeenCalledWith(true);
});

it("points the header's aria-controls at the content", async () => {
const user = userEvent.setup();

render(
<ChainOfThought>
<ChainOfThoughtHeader />
<ChainOfThoughtContent>Steps</ChainOfThoughtContent>
</ChainOfThought>
);

const trigger = screen.getByRole("button");
await user.click(trigger);

expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(trigger).toHaveAttribute(
"aria-controls",
screen.getByText("Steps").id
);
});
});

describe("chainOfThoughtHeader", () => {
Expand Down
69 changes: 35 additions & 34 deletions packages/elements/src/chain-of-thought.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ export const ChainOfThought = memo(

return (
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
<div className={cn("not-prose w-full space-y-4", className)} {...props}>
<Collapsible
className={cn("not-prose w-full space-y-4", className)}
onOpenChange={setIsOpen}
open={isOpen}
{...props}
>
{children}
</div>
</Collapsible>
</ChainOfThoughtContext.Provider>
);
}
Expand All @@ -74,29 +79,27 @@ export type ChainOfThoughtHeaderProps = ComponentProps<

export const ChainOfThoughtHeader = memo(
({ className, children, ...props }: ChainOfThoughtHeaderProps) => {
const { isOpen, setIsOpen } = useChainOfThought();
const { isOpen } = useChainOfThought();

return (
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
<CollapsibleTrigger
<CollapsibleTrigger
className={cn(
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
className
)}
{...props}
>
<BrainIcon className="size-4" />
<span className="flex-1 text-left">
{children ?? "Chain of Thought"}
</span>
<ChevronDownIcon
className={cn(
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
className
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
)}
{...props}
>
<BrainIcon className="size-4" />
<span className="flex-1 text-left">
{children ?? "Chain of Thought"}
</span>
<ChevronDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0"
)}
/>
</CollapsibleTrigger>
</Collapsible>
/>
</CollapsibleTrigger>
);
}
);
Expand Down Expand Up @@ -179,21 +182,19 @@ export type ChainOfThoughtContentProps = ComponentProps<

export const ChainOfThoughtContent = memo(
({ className, children, ...props }: ChainOfThoughtContentProps) => {
const { isOpen } = useChainOfThought();
useChainOfThought();

return (
<Collapsible open={isOpen}>
<CollapsibleContent
className={cn(
"mt-2 space-y-3",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
className
)}
{...props}
>
{children}
</CollapsibleContent>
</Collapsible>
<CollapsibleContent
className={cn(
"mt-2 space-y-3",
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
className
)}
{...props}
>
{children}
</CollapsibleContent>
);
}
);
Expand Down