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
64 changes: 64 additions & 0 deletions packages/react/accordion/src/accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,51 @@ describe('given a single Accordion', () => {
});
});

describe('given a controlled single collapsible Accordion', () => {
let rendered: RenderResult;

afterEach(cleanup);

beforeEach(() => {
rendered = render(<ControlledAccordionSingle />);
});

describe('when opening an item and then clicking the same trigger', () => {
let trigger: HTMLElement;
let contentOne: HTMLElement;

beforeEach(() => {
trigger = rendered.getByText('Trigger One');
fireEvent.click(trigger);
contentOne = rendered.getByText('Content One');
});

it('should show the content after the first click', () => {
expect(contentOne).toBeVisible();
});

describe('then clicking the trigger again to close', () => {
beforeEach(() => {
fireEvent.click(trigger);
});

it('should hide the content after the second click', () => {
expect(contentOne).not.toBeVisible();
});

describe('then clicking the trigger a third time to re-open', () => {
beforeEach(() => {
fireEvent.click(trigger);
});

it('should show the content again', () => {
expect(rendered.getByText('Content One')).toBeVisible();
});
});
});
});
});

describe('given a multiple Accordion', () => {
let handleValueChange: Mock;
let rendered: RenderResult;
Expand Down Expand Up @@ -412,3 +457,22 @@ function AccordionTest(props: React.ComponentProps<typeof Accordion.Root>) {
</Accordion.Root>
);
}

function ControlledAccordionSingle() {
const [value, setValue] = React.useState<string | undefined>(undefined);
return (
<Accordion.Root
type="single"
collapsible
value={value}
onValueChange={(v) => setValue(v || undefined)}
>
<Accordion.Item value="One">
<Accordion.Header>
<Accordion.Trigger>Trigger One</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>Content One</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
}
8 changes: 7 additions & 1 deletion packages/react/accordion/src/accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ const AccordionImplSingle = React.forwardRef<AccordionImplSingleElement, Accordi
...accordionSingleProps
} = props;

// When the `value` prop is explicitly provided (even as `undefined`), the
// component is in controlled mode and we normalize `undefined` to `''` so
// that `useControllableState` always sees a non-undefined prop. This
// prevents the controlled ↔ uncontrolled flip that would otherwise cause
// the stale uncontrolled state to re-open the item after collapse.
const isValueControlled = 'value' in props;
const [value, setValue] = useControllableState({
prop: valueProp,
prop: isValueControlled ? (valueProp ?? '') : valueProp,
defaultProp: defaultValue ?? '',
onChange: onValueChange,
caller: ACCORDION_NAME,
Expand Down