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
6 changes: 6 additions & 0 deletions .changeset/tame-carpets-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@radix-ui/react-collapsible': minor
'@radix-ui/react-accordion': minor
---

add prop keepChildrenMounted
88 changes: 88 additions & 0 deletions apps/storybook/stories/accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,94 @@ export const Horizontal = () => (
</>
);

export const KeepChildrenMounted = () => (
<>
<h1>Single/Uncontrolled</h1>
<Accordion.Root keepChildrenMounted type="single" className={styles.root}>
<Accordion.Item className={styles.item} value="one">
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>One</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Per erat orci nostra luctus sociosqu mus risus penatibus, duis elit vulputate viverra
integer ullamcorper congue curabitur sociis, nisi malesuada scelerisque quam suscipit
habitant sed.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className={styles.item} value="two">
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>Two</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Cursus sed mattis commodo fermentum conubia ipsum pulvinar sagittis, diam eget bibendum
porta nascetur ac dictum, leo tellus dis integer platea ultrices mi.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className={styles.item} value="three" disabled>
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>Three (disabled)</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Sociis hac sapien turpis conubia sagittis justo dui, inceptos penatibus feugiat himenaeos
euismod magna, nec tempor pulvinar eu etiam mattis.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className={styles.item} value="four">
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>Four</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Odio placerat <a href="#">quisque</a> sapien sagittis non sociis ligula penatibus
dignissim vitae, enim vulputate nullam semper potenti etiam volutpat libero.
<button>Cool</button>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>

<h1>Multiple/Uncontrolled</h1>
<Accordion.Root keepChildrenMounted type="multiple" className={styles.root}>
<Accordion.Item className={styles.item} value="one">
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>One</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Per erat orci nostra luctus sociosqu mus risus penatibus, duis elit vulputate viverra
integer ullamcorper congue curabitur sociis, nisi malesuada scelerisque quam suscipit
habitant sed.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className={styles.item} value="two">
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>Two</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Cursus sed mattis commodo fermentum conubia ipsum pulvinar sagittis, diam eget bibendum
porta nascetur ac dictum, leo tellus dis integer platea ultrices mi.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className={styles.item} value="three" disabled>
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>Three (disabled)</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Sociis hac sapien turpis conubia sagittis justo dui, inceptos penatibus feugiat himenaeos
euismod magna, nec tempor pulvinar eu etiam mattis.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item className={styles.item} value="four">
<Accordion.Header className={styles.header}>
<Accordion.Trigger className={styles.trigger}>Four</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content className={styles.content}>
Odio placerat <a href="#">quisque</a> sapien sagittis non sociis ligula penatibus
dignissim vitae, enim vulputate nullam semper potenti etiam volutpat libero.
<button>Cool</button>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</>
);

export const Chromatic = () => {
const items = ['One', 'Two', 'Three', 'Four'];
return (
Expand Down
10 changes: 10 additions & 0 deletions apps/storybook/stories/collapsible.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ export const AnimatedHorizontal = () => {
);
};

export const KeepChildrenMounted = () => (
<>
<h1>With keepChildrenMounted</h1>
<Collapsible.Root keepChildrenMounted className={styles.root}>
<Collapsible.Trigger className={styles.trigger}>Trigger</Collapsible.Trigger>
<Collapsible.Content className={styles.content}>Content 1</Collapsible.Content>
</Collapsible.Root>
</>
);

export const Chromatic = () => (
<>
<h1>Uncontrolled</h1>
Expand Down
17 changes: 16 additions & 1 deletion packages/react/accordion/src/accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ type AccordionImplContextValue = {
disabled?: boolean;
direction: AccordionImplProps['dir'];
orientation: AccordionImplProps['orientation'];
keepChildrenMounted?: boolean;
};

const [AccordionImplProvider, useAccordionContext] =
Expand All @@ -219,11 +220,23 @@ interface AccordionImplProps extends PrimitiveDivProps {
* The language read direction.
*/
dir?: Direction;
/**
* When set to true, children of content will remain mounted when content is
* collapsed.
*/
keepChildrenMounted?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention in radix is to use forceMount for such things and it should be closest to the source which is AccordionContent

But AccordionContent already supports all of CollapsibleContent props and by extension also supports the forceMount prop

interface AccordionContentProps extends CollapsibleContentProps {}

I’m curious why don’t those existing props solve your current problem?

}

const AccordionImpl = React.forwardRef<AccordionImplElement, AccordionImplProps>(
(props: ScopedProps<AccordionImplProps>, forwardedRef) => {
const { __scopeAccordion, disabled, dir, orientation = 'vertical', ...accordionProps } = props;
const {
__scopeAccordion,
disabled,
dir,
orientation = 'vertical',
keepChildrenMounted,
...accordionProps
} = props;
const accordionRef = React.useRef<AccordionImplElement>(null);
const composedRefs = useComposedRefs(accordionRef, forwardedRef);
const getItems = useCollection(__scopeAccordion);
Expand Down Expand Up @@ -307,6 +320,7 @@ const AccordionImpl = React.forwardRef<AccordionImplElement, AccordionImplProps>
disabled={disabled}
direction={dir}
orientation={orientation}
keepChildrenMounted={keepChildrenMounted}
>
<Collection.Slot scope={__scopeAccordion}>
<Primitive.div
Expand Down Expand Up @@ -373,6 +387,7 @@ const AccordionItem = React.forwardRef<AccordionItemElement, AccordionItemProps>
{...collapsibleScope}
{...accordionItemProps}
ref={forwardedRef}
keepChildrenMounted={accordionContext.keepChildrenMounted}
disabled={disabled}
open={open}
onOpenChange={(open) => {
Expand Down
38 changes: 38 additions & 0 deletions packages/react/collapsible/src/collapsible.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,41 @@ describe('given an open controlled Collapsible', () => {
});
});
});

describe('given a collapsible with keepChildrenMounted', () => {
let rendered: RenderResult;
let content: HTMLElement;

afterEach(cleanup);

beforeEach(() => {
rendered = render(<CollapsibleTest keepChildrenMounted />);
content = rendered.getByText(CONTENT_TEXT);
});

describe('when clicking the trigger', () => {
beforeEach(() => {
const trigger = rendered.getByText(TRIGGER_TEXT);
fireEvent.click(trigger);
});

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

describe('and clicking the trigger again', () => {
beforeEach(() => {
const trigger = rendered.getByText(TRIGGER_TEXT);
fireEvent.click(trigger);
});

it('should close the content', () => {
expect(content).not.toBeVisible();
});

it('should keep the children mounted', () => {
expect(content).toBeInTheDocument();
});
});
});
})
10 changes: 9 additions & 1 deletion packages/react/collapsible/src/collapsible.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const [createCollapsibleContext, createCollapsibleScope] = createContextScope(CO

type CollapsibleContextValue = {
contentId: string;
keepChildrenMounted?: boolean;
disabled?: boolean;
open: boolean;
onOpenToggle(): void;
Expand All @@ -33,6 +34,11 @@ type CollapsibleElement = React.ComponentRef<typeof Primitive.div>;
type PrimitiveDivProps = React.ComponentPropsWithoutRef<typeof Primitive.div>;
interface CollapsibleProps extends PrimitiveDivProps {
defaultOpen?: boolean;
/**
* When set to true, children of content will remain mounted when content is
* collapsed.
*/
keepChildrenMounted?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collapsible already supports this viaforceMount

So this prop is kinda redundant here

open?: boolean;
disabled?: boolean;
onOpenChange?(open: boolean): void;
Expand All @@ -44,6 +50,7 @@ const Collapsible = React.forwardRef<CollapsibleElement, CollapsibleProps>(
__scopeCollapsible,
open: openProp,
defaultOpen,
keepChildrenMounted,
disabled,
onOpenChange,
...collapsibleProps
Expand All @@ -59,6 +66,7 @@ const Collapsible = React.forwardRef<CollapsibleElement, CollapsibleProps>(
return (
<CollapsibleProvider
scope={__scopeCollapsible}
keepChildrenMounted={keepChildrenMounted}
disabled={disabled}
contentId={useId()}
open={open}
Expand Down Expand Up @@ -217,7 +225,7 @@ const CollapsibleContentImpl = React.forwardRef<
...props.style,
}}
>
{isOpen && children}
{(isOpen || context.keepChildrenMounted) && children}
</Primitive.div>
);
});
Expand Down