Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions apps/storybook/src/Accordion.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,10 @@ WithArrowIcon.storyName = "With arrow icon";
WithArrowIcon.args = {
arrowIcon: HiOutlineArrowCircleDown,
};

export const WithAnimation = Template.bind({});
WithAnimation.storyName = "With animation";
WithAnimation.args = {
animate: true,
animationDuration: 300,
};
6 changes: 6 additions & 0 deletions apps/web/content/docs/components/accordion.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ Use this example to automatically collapse all of the accordion panels by passin

<Example name="accordion.collapseAll" />

## With animation

Enable smooth open/close animation by passing `animate` and `animationDuration` (in milliseconds) to the `<Accordion>` component.

<Example name="accordion.animation" />

## Theme

To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme).
Expand Down
163 changes: 163 additions & 0 deletions apps/web/examples/accordion/accordion.animation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Accordion, AccordionContent, AccordionPanel, AccordionTitle } from "flowbite-react";
import type { CodeData } from "~/components/code-demo";

const code = `
import { Accordion, AccordionContent, AccordionPanel, AccordionTitle } from "flowbite-react";

export function Component() {
return (
<Accordion animate animationDuration={300}>
<AccordionPanel>
<AccordionTitle>What is Flowbite?</AccordionTitle>
<AccordionContent>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is an open-source library of interactive components built on top of Tailwind CSS including buttons,
dropdowns, modals, navbars, and more.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out this guide to learn how to&nbsp;
<a
href="https://flowbite.com/docs/getting-started/introduction/"
className="text-cyan-600 hover:underline dark:text-cyan-500"
>
get started&nbsp;
</a>
and start developing websites even faster with components on top of Tailwind CSS.
</p>
</AccordionContent>
</AccordionPanel>
<AccordionPanel>
<AccordionTitle>Is there a Figma file available?</AccordionTitle>
<AccordionContent>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is first conceptualized and designed using the Figma software so everything you see in the library
has a design equivalent in our Figma file.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out the
<a href="https://flowbite.com/figma/" className="text-cyan-600 hover:underline dark:text-cyan-500">
Figma design system
</a>
based on the utility classes from Tailwind CSS and components from Flowbite.
</p>
</AccordionContent>
</AccordionPanel>
<AccordionPanel>
<AccordionTitle>What are the differences between Flowbite and Tailwind UI?</AccordionTitle>
<AccordionContent>
<p className="mb-2 text-gray-500 dark:text-gray-400">
The main difference is that the core components from Flowbite are open source under the MIT license, whereas
Tailwind UI is a paid product. Another difference is that Flowbite relies on smaller and standalone
components, whereas Tailwind UI offers sections of pages.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">
However, we actually recommend using both Flowbite, Flowbite Pro, and even Tailwind UI as there is no
technical reason stopping you from using the best of two worlds.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">Learn more about these technologies:</p>
<ul className="list-disc pl-5 text-gray-500 dark:text-gray-400">
<li>
<a href="https://flowbite.com/pro/" className="text-cyan-600 hover:underline dark:text-cyan-500">
Flowbite Pro
</a>
</li>
<li>
<a
href="https://tailwindui.com/"
rel="nofollow"
className="text-cyan-600 hover:underline dark:text-cyan-500"
>
Tailwind UI
</a>
</li>
</ul>
</AccordionContent>
</AccordionPanel>
</Accordion>
);
}
`;

export function Component() {
return (
<Accordion animate animationDuration={300}>
<AccordionPanel>
<AccordionTitle>What is Flowbite?</AccordionTitle>
<AccordionContent>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is an open-source library of interactive components built on top of Tailwind CSS including buttons,
dropdowns, modals, navbars, and more.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out this guide to learn how to&nbsp;
<a
href="https://flowbite.com/docs/getting-started/introduction/"
className="text-cyan-600 hover:underline dark:text-cyan-500"
>
get started&nbsp;
</a>
and start developing websites even faster with components on top of Tailwind CSS.
</p>
</AccordionContent>
</AccordionPanel>
<AccordionPanel>
<AccordionTitle>Is there a Figma file available?</AccordionTitle>
<AccordionContent>
<p className="mb-2 text-gray-500 dark:text-gray-400">
Flowbite is first conceptualized and designed using the Figma software so everything you see in the library
has a design equivalent in our Figma file.
</p>
<p className="text-gray-500 dark:text-gray-400">
Check out the
<a href="https://flowbite.com/figma/" className="text-cyan-600 hover:underline dark:text-cyan-500">
Figma design system
</a>
based on the utility classes from Tailwind CSS and components from Flowbite.
</p>
</AccordionContent>
</AccordionPanel>
<AccordionPanel>
<AccordionTitle>What are the differences between Flowbite and Tailwind UI?</AccordionTitle>
<AccordionContent>
<p className="mb-2 text-gray-500 dark:text-gray-400">
The main difference is that the core components from Flowbite are open source under the MIT license, whereas
Tailwind UI is a paid product. Another difference is that Flowbite relies on smaller and standalone
components, whereas Tailwind UI offers sections of pages.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">
However, we actually recommend using both Flowbite, Flowbite Pro, and even Tailwind UI as there is no
technical reason stopping you from using the best of two worlds.
</p>
<p className="mb-2 text-gray-500 dark:text-gray-400">Learn more about these technologies:</p>
<ul className="list-disc pl-5 text-gray-500 dark:text-gray-400">
<li>
<a href="https://flowbite.com/pro/" className="text-cyan-600 hover:underline dark:text-cyan-500">
Flowbite Pro
</a>
</li>
<li>
<a
href="https://tailwindui.com/"
rel="nofollow"
className="text-cyan-600 hover:underline dark:text-cyan-500"
>
Tailwind UI
</a>
</li>
</ul>
</AccordionContent>
</AccordionPanel>
</Accordion>
);
}

export const animation: CodeData = {
type: "single",
code: {
fileName: "index",
language: "tsx",
code,
},
githubSlug: "accordion/accordion.animation.tsx",
component: <Component />,
};
1 change: 1 addition & 0 deletions apps/web/examples/accordion/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { animation } from "./accordion.animation";
export { collapseAll } from "./accordion.collapseAll";
export { root } from "./accordion.root";
29 changes: 29 additions & 0 deletions packages/ui/src/components/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,35 @@ describe("Components / Accordion", () => {
expect(content()[1]).not.toBeVisible(); // content should not be visible
});
});

describe("Animation", () => {
it("should apply transition styles when `animate` is enabled", () => {
render(<TestAccordion animate animationDuration={400} />);

const animatedContent = content()[0];

expect(animatedContent).toHaveStyle("transition: max-height 400ms ease-out");
expect(animatedContent).toHaveAttribute("aria-hidden", "false");
});

it("should not break the animation when `animate` is disabled", () => {
render(<TestAccordion />);

const firstContent = content()[0];

expect(firstContent).not.toHaveStyle("transition: max-height 400ms ease-out");
expect(firstContent).not.toHaveAttribute("aria-hidden", "false");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("should use the default `animationDuration` when `animate` is enabled and `animationDuration` is not provided", () => {
render(<TestAccordion animate />);

const firstContent = content()[0];

expect(firstContent).toHaveStyle("transition: max-height 300ms ease-out");
expect(firstContent).toHaveAttribute("aria-hidden", "false");
});
});
});

const TestAccordion = (props: Omit<AccordionProps, "children">) => (
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface AccordionRootTheme {

export interface AccordionProps extends ComponentProps<"div">, ThemingProps<AccordionRootTheme> {
alwaysOpen?: boolean;
animate?: boolean;
animationDuration?: number;
arrowIcon?: FC<ComponentProps<"svg">>;
children: ReactElement<AccordionPanelProps> | ReactElement<AccordionPanelProps>[];
flush?: boolean;
Expand All @@ -43,6 +45,8 @@ export function Accordion(props: AccordionProps) {

const {
alwaysOpen = false,
animate = false,
animationDuration = 300,
Comment on lines +50 to +51
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard animationDuration against invalid runtime values.

Line 51 defaults only when the prop is undefined; values like negative numbers/NaN still flow through and can produce broken transition behavior downstream. Add a small clamp/sanitize step before passing it to panels.

Suggested fix
-  const {
+  const {
     alwaysOpen = false,
     animate = false,
-    animationDuration = 300,
+    animationDuration: animationDurationProp = 300,
     arrowIcon = ChevronDownIcon,
     children,
     flush = false,
     collapseAll = false,
     className,
     ...restProps
   } = resolveProps(props, provider.props?.accordion);
+
+  const animationDuration =
+    Number.isFinite(animationDurationProp) && animationDurationProp >= 0 ? animationDurationProp : 300;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/Accordion/Accordion.tsx` around lines 50 - 51,
Sanitize the animationDuration prop inside the Accordion component before
passing it to child panels: ensure animationDuration is a finite number and
non-negative (e.g., if not Number.isFinite(animationDuration) or
animationDuration < 0, use the default 300), then pass that sanitized value to
panels/AccordionPanel; do this in the Accordion function body (where animate and
animationDuration are de/structured) so downstream transition code never
receives NaN or negative durations.

arrowIcon = ChevronDownIcon,
children,
flush = false,
Expand All @@ -58,13 +62,15 @@ export function Accordion(props: AccordionProps) {
Children.map(children, (child, i) =>
cloneElement(child, {
alwaysOpen,
animate,
animationDuration,
arrowIcon,
flush,
isOpen: isOpen === i,
setOpen: () => setOpen(isOpen === i ? -1 : i),
}),
),
[alwaysOpen, arrowIcon, children, flush, isOpen],
[alwaysOpen, animate, animationDuration, arrowIcon, children, flush, isOpen],
);

return (
Expand Down
30 changes: 28 additions & 2 deletions packages/ui/src/components/Accordion/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import type { ComponentProps } from "react";
import { useLayoutEffect, useRef, useState } from "react";
import { get } from "../../helpers/get";
import { resolveProps } from "../../helpers/resolve-props";
import { useResolveTheme } from "../../helpers/resolve-theme";
Expand All @@ -17,7 +18,15 @@ export interface AccordionContentTheme {
export interface AccordionContentProps extends ComponentProps<"div">, ThemingProps<AccordionContentTheme> {}

export function AccordionContent(props: AccordionContentProps) {
const { isOpen } = useAccordionContext();
const { isOpen, animate = false, animationDuration = 300 } = useAccordionContext();
const contentRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<number>(0);

useLayoutEffect(() => {
if (!animate || !isOpen) return;
const el = contentRef.current;
if (el) setHeight(el.scrollHeight);
}, [animate, isOpen]);

const provider = useThemeProvider();
const theme = useResolveTheme(
Expand All @@ -28,13 +37,30 @@ export function AccordionContent(props: AccordionContentProps) {

const { className, ...restProps } = resolveProps(props, provider.props?.accordionContent);

return (
const handleTransitionEnd = () => {
if (!isOpen) setHeight(0);
};

return !animate ? (
<div
className={twMerge(theme.base, className)}
data-testid="flowbite-accordion-content"
hidden={!isOpen}
{...restProps}
/>
) : (
<div
data-testid="flowbite-accordion-content"
aria-hidden={!isOpen}
style={{
overflow: "hidden",
maxHeight: isOpen ? height : 0,
transition: `max-height ${animationDuration}ms ease-out`,
}}
onTransitionEnd={handleTransitionEnd}
>
<div ref={contentRef} className={twMerge(theme.base, className)} {...restProps} />
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
}

Expand Down
Loading