Skip to content

Commit 1ece137

Browse files
committed
first step accordion
1 parent 8dd0e64 commit 1ece137

File tree

5 files changed

+198
-1
lines changed

5 files changed

+198
-1
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@
126126
"./Tabs": "./dist/Tabs.js",
127127
"./Header": "./dist/Header.js",
128128
"./DarkModeSwitch": "./dist/DarkModeSwitch.js",
129-
"./Alert": "./dist/Alert.js"
129+
"./Alert": "./dist/Alert.js",
130+
"./AccordionGroup": "./dist/AccordionGroup.js",
131+
"./Accordion": "./dist/Accordion.js"
130132
}
131133
}

src/Accordion.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { forwardRef, memo, ReactNode, useId } from "react";
2+
import { assert } from "tsafe";
3+
import type { Equals } from "tsafe";
4+
import { fr } from "./lib";
5+
import { cx } from "./lib/tools/cx";
6+
import "@gouvfr/dsfr/dist/component/accordion/accordion.css";
7+
import { symToStr } from "tsafe/symToStr";
8+
9+
export type AccordionProps = AccordionProps.Controlled;
10+
11+
//TODO Controlled mode (callback onClick, expended etc ...)
12+
export namespace AccordionProps {
13+
export type Common = {
14+
className?: string;
15+
titleAs?: `h${2 | 3 | 4 | 5 | 6}`;
16+
label: ReactNode;
17+
classes?: Partial<Record<"root" | "accordion" | "title" | "collapse", string>>;
18+
content: NonNullable<ReactNode>;
19+
};
20+
21+
export type Controlled = Common & {};
22+
}
23+
24+
/** @see <https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/accordeon> */
25+
export const Accordion = memo(
26+
forwardRef<HTMLDivElement, AccordionProps>((props, ref) => {
27+
const {
28+
className,
29+
titleAs: HtmlTitleTag = "h3",
30+
label,
31+
classes = {},
32+
content,
33+
...rest
34+
} = props;
35+
36+
assert<Equals<keyof typeof rest, never>>();
37+
38+
const id = useId();
39+
40+
return (
41+
<section className={cx(fr.cx("fr-accordion"), className)} ref={ref} {...rest}>
42+
<HtmlTitleTag className={cx(fr.cx("fr-accordion__title"), classes.title)}>
43+
<button
44+
className={fr.cx("fr-accordion__btn")}
45+
aria-expanded="true"
46+
aria-controls={`accordion-${id}`}
47+
>
48+
{label}
49+
</button>
50+
</HtmlTitleTag>
51+
<div className={cx(fr.cx("fr-collapse"), classes.collapse)} id={`accordion-${id}`}>
52+
{content}
53+
</div>
54+
</section>
55+
);
56+
})
57+
);
58+
59+
Accordion.displayName = symToStr({ Accordion });
60+
61+
export default Accordion;

src/AccordionGroup.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { forwardRef, memo } from "react";
2+
import { assert } from "tsafe";
3+
import type { Equals } from "tsafe";
4+
import { fr } from "./lib";
5+
import { cx } from "./lib/tools/cx";
6+
import "@gouvfr/dsfr/dist/component/accordion/accordion.css";
7+
import { symToStr } from "tsafe/symToStr";
8+
import { Accordion, AccordionProps } from "./Accordion";
9+
10+
export type AccordionGroup = AccordionGroup.Controlled;
11+
12+
export namespace AccordionGroup {
13+
export type Common = {
14+
className?: string;
15+
accordions: AccordionProps[];
16+
};
17+
18+
export type Controlled = Common & {};
19+
}
20+
21+
/** @see <https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/accordeon> */
22+
export const AccordionGroup = memo(
23+
forwardRef<HTMLDivElement, AccordionGroup>((props, ref) => {
24+
const { className, accordions, ...rest } = props;
25+
26+
assert<Equals<keyof typeof rest, never>>();
27+
28+
return (
29+
<div className={cx(fr.cx("fr-accordions-group"), className)} ref={ref} {...rest}>
30+
{accordions.map(({ label, content, className, titleAs, classes = {} }) => (
31+
<Accordion
32+
label={label}
33+
content={content}
34+
titleAs={titleAs}
35+
classes={classes}
36+
className={className}
37+
/>
38+
))}
39+
</div>
40+
);
41+
})
42+
);
43+
44+
AccordionGroup.displayName = symToStr({ AccordionGroup });
45+
46+
export default AccordionGroup;

stories/Accordion.stories.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
import { Accordion } from "../dist/Accordion";
3+
import { sectionName } from "./sectionName";
4+
import { getStoryFactory, logCallbacks } from "./getStory";
5+
6+
const { meta, getStory } = getStoryFactory({
7+
sectionName,
8+
"wrappedComponent": { Accordion },
9+
"description": `- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/accordeon)
10+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Accordion.tsx)
11+
12+
## Controlled
13+
14+
In this mode you are in charge of the behavior of the Accordion.
15+
_NOTE: In controlled mode there is no animation transition when expanding or colapsing the accordion._
16+
17+
\`\`\`tsx
18+
function ControlledAccordion() {
19+
20+
return (
21+
<Accordion label="Name of the Accordion" content="Content of the Accordion"/>
22+
);
23+
24+
}
25+
\`\`\``,
26+
"disabledProps": ["lang"]
27+
});
28+
29+
export default meta;
30+
31+
export const Default = getStory({
32+
"label": "Name of the Accordion",
33+
"content": "Content of the Accordion"
34+
});

stories/AccordionGroup.stories.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from "react";
2+
import { AccordionGroup } from "../dist/AccordionGroup";
3+
import { sectionName } from "./sectionName";
4+
import { getStoryFactory, logCallbacks } from "./getStory";
5+
6+
//TODO -> Do only one story for Accordion and AccordionGroup ?
7+
8+
const { meta, getStory } = getStoryFactory({
9+
sectionName,
10+
"wrappedComponent": { AccordionGroup },
11+
"description": `- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/accordeon)
12+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Accordion.tsx)
13+
14+
## Controlled
15+
16+
In this mode you are in charge of the behavior of the Accordion.
17+
_NOTE: In controlled mode there is no animation transition when expanding or colapsing the accordion._
18+
19+
\`\`\`tsx
20+
function ControlledAccordionGroup() {
21+
22+
return (
23+
<AccordionGroup accordions={[
24+
{
25+
label: "Name of the Accordion 1",
26+
content: "Content of the Accordion 1"
27+
},
28+
{
29+
label: "Name of the Accordion 2",
30+
content: "Content of the Accordion 2"
31+
}
32+
]}
33+
/>
34+
);
35+
36+
}
37+
\`\`\``,
38+
"disabledProps": ["lang"]
39+
});
40+
41+
export default meta;
42+
43+
export const Default = getStory({
44+
accordions: [
45+
{
46+
label: "Name of the Accordion 1",
47+
content: "Content of the Accordion 1"
48+
},
49+
{
50+
label: "Name of the Accordion 2",
51+
content: "Content of the Accordion 2"
52+
}
53+
]
54+
});

0 commit comments

Comments
 (0)