Skip to content
Closed
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/shy-kiwis-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@heroui/accordion": patch
---

- Introduce new prop `scrollOnOpen?: boolean` to automatically scroll to the content when expanded
- Introduce new prop `transitionDuration?: number` to customize animation speed. Defaults to 300ms as it is right now
4 changes: 4 additions & 0 deletions apps/docs/content/components/accordion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import indicatorFunction from "./indicator-function";
import customMotion from "./custom-motion";
import controlled from "./controlled";
import customStyles from "./custom-styles";
import transitionDuration from "./transition-duration";
import scrollOnOpen from "./scroll-on-open";

export const accordionContent = {
usage,
Expand All @@ -32,4 +34,6 @@ export const accordionContent = {
customMotion,
controlled,
customStyles,
transitionDuration,
scrollOnOpen,
};
65 changes: 65 additions & 0 deletions apps/docs/content/components/accordion/scroll-on-open.raw.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {Accordion, AccordionItem, Button} from "@heroui/react";

export default function App() {
const defaultContent =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.";

return (
<div className="w-full grid grid-cols-12 gap-4">
<div className="col-span-12 lg:col-span-6 flex flex-col gap-4">
<h3>Without Scroll on Open</h3>
<div className="h-64 overflow-auto p-4 border rounded-lg">
<div className="mb-32">
<p className="mb-4 text-sm text-gray-600">
Scroll down to see the accordion. When you expand items, they won&apos;t automatically
scroll into view.
</p>
</div>
<Accordion>
<AccordionItem key="1" aria-label="Accordion 1" title="Regular Accordion Item">
<div className="p-2">
<p className="mb-4">{defaultContent}</p>
<Button color="primary" size="sm">
This content might be out of view
</Button>
</div>
</AccordionItem>
<AccordionItem key="2" aria-label="Accordion 2" title="Another Item">
{defaultContent}
</AccordionItem>
<AccordionItem key="3" aria-label="Accordion 3" title="Third Item">
{defaultContent}
</AccordionItem>
</Accordion>
</div>
</div>
<div className="col-span-12 lg:col-span-6 flex flex-col gap-4">
<h3>With Scroll on Open</h3>
<div className="h-64 overflow-auto p-4 border rounded-lg">
<div className="mb-32">
<p className="mb-4 text-sm text-gray-600">
Scroll down to see the accordion. When you expand items, they will automatically
scroll into view.
</p>
</div>
<Accordion scrollOnOpen>
<AccordionItem key="1" aria-label="Accordion 1" title="Auto-Scroll Accordion Item">
<div className="p-2">
<p className="mb-4">{defaultContent}</p>
<Button color="primary" size="sm">
This will be scrolled into view
</Button>
</div>
</AccordionItem>
<AccordionItem key="2" aria-label="Accordion 2" title="Another Auto-Scroll Item">
{defaultContent}
</AccordionItem>
<AccordionItem key="3" aria-label="Accordion 3" title="Third Auto-Scroll Item">
{defaultContent}
</AccordionItem>
</Accordion>
</div>
</div>
</div>
);
}
9 changes: 9 additions & 0 deletions apps/docs/content/components/accordion/scroll-on-open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./scroll-on-open.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
67 changes: 67 additions & 0 deletions apps/docs/content/components/accordion/transition-duration.raw.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {Accordion, AccordionItem} from "@heroui/react";

export default function App() {
const defaultContent =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.";

return (
<div className="w-full grid grid-cols-12 gap-4">
<div className="col-span-12 lg:col-span-6 flex flex-col gap-4">
<h3>Fast Transition (150ms)</h3>
<Accordion transitionDuration={150}>
<AccordionItem key="1" aria-label="Accordion 1" title="Fast Animation">
{defaultContent}
</AccordionItem>
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
{defaultContent}
</AccordionItem>
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
{defaultContent}
</AccordionItem>
</Accordion>
</div>
<div className="col-span-12 lg:col-span-6 flex flex-col gap-4">
<h3>Default Transition (300ms)</h3>
<Accordion>
<AccordionItem key="1" aria-label="Accordion 1" title="Default Animation">
{defaultContent}
</AccordionItem>
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
{defaultContent}
</AccordionItem>
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
{defaultContent}
</AccordionItem>
</Accordion>
</div>
<div className="col-span-12 lg:col-span-6 flex flex-col gap-4">
<h3>Slow Transition (800ms)</h3>
<Accordion transitionDuration={800}>
<AccordionItem key="1" aria-label="Accordion 1" title="Slow Animation">
{defaultContent}
</AccordionItem>
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
{defaultContent}
</AccordionItem>
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
{defaultContent}
</AccordionItem>
</Accordion>
</div>
<div className="col-span-12 lg:col-span-6 flex flex-col gap-4">
<h3>Very Slow Transition (1200ms)</h3>
<Accordion transitionDuration={1200}>
<AccordionItem key="1" aria-label="Accordion 1" title="Very Slow Animation">
{defaultContent}
</AccordionItem>
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
{defaultContent}
</AccordionItem>
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
{defaultContent}
</AccordionItem>
</Accordion>
</div>
</div>
);
}
9 changes: 9 additions & 0 deletions apps/docs/content/components/accordion/transition-duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./transition-duration.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
24 changes: 24 additions & 0 deletions apps/docs/content/docs/components/accordion.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ Accordion offers a `motionProps` property to customize the `enter` / `exit` anim

> Learn more about Framer motion variants [here](https://www.framer.com/motion/animation/#variants).

### Transition Duration

Use `transitionDuration` property to customize the animations duration.

<CodeDemo title="Transition Duration" files={accordionContent.transitionDuration} />

### Scroll on Open

Use `scrollOnOpen` property to automatically scroll to the content when an accordion item is expanded.

<CodeDemo title="Scroll on Open" files={accordionContent.scrollOnOpen} />

### Controlled

Accordion is a controlled component, which means you need to control the `selectedKeys` property by yourself.
Expand Down Expand Up @@ -281,6 +293,18 @@ Here's an example of how to customize the accordion styles:
description: "The motion properties of the Accordion.",
default: "-"
},
{
attribute: "transitionDuration",
type: "number",
description: "The duration of the animations in milliseconds.",
default: 300
},
{
attribute: "scrollOnOpen",
type: "boolean",
description: "Whether to automatically scroll to the content when an accordion item is expanded.",
default: false
},
{
attribute: "disabledKeys",
type: "React.Key[]",
Expand Down
62 changes: 62 additions & 0 deletions packages/components/accordion/__tests__/accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,4 +438,66 @@ describe("Accordion", () => {

expect(getByRole("separator")).toHaveClass("bg-rose-500");
});

it("should scroll to content when scrollOnOpen is true", async () => {
const scrollIntoViewMock = jest.fn();

Element.prototype.scrollIntoView = scrollIntoViewMock;

const wrapper = render(
<Accordion scrollOnOpen>
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
Accordion Item 1 description
</AccordionItem>
</Accordion>,
);

const first = wrapper.getByTestId("item-1");
const firstButton = first.querySelector("button") as HTMLElement;

await user.click(firstButton);

await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
});

expect(scrollIntoViewMock).toHaveBeenCalledWith(
expect.objectContaining({
behavior: "smooth",
block: "nearest",
}),
);

jest.restoreAllMocks();
});

it("should apply custom transition duration", async () => {
const customDuration = 500;
const wrapper = render(
<Accordion disableAnimation={false} transitionDuration={customDuration}>
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
Accordion Item 1 description
</AccordionItem>
</Accordion>,
);

const first = wrapper.getByTestId("item-1");
const firstButton = first.querySelector("button") as HTMLElement;

await user.click(firstButton);

const content = first.querySelector("section");

expect(content).toBeInTheDocument();
// During animation the opacity changes from 0 to 1, reaching number close to 1 on the end
expect(content).toHaveAttribute("style", expect.stringMatching("opacity: 0"));

// Allow framer-motion time to apply animation styles
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, customDuration + 10));
});

// Match opacity values between 0.98 and 1 (inclusive)
expect(content).toHaveAttribute("style", expect.stringMatching(/opacity: (0\.9[8-9]\d*|1)/));
});
});
Loading