Skip to content

Commit 67ee35c

Browse files
authored
Changes to collapsible component #528 (#677)
1 parent 91ba422 commit 67ee35c

File tree

12 files changed

+478
-94
lines changed

12 files changed

+478
-94
lines changed
Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import React, { PropsWithChildren, ReactNode, useState } from 'react';
2-
import ButtonPrimitive from '~/core/primitives/Button';
1+
import React, {
2+
Dispatch,
3+
PropsWithChildren,
4+
ReactNode,
5+
SetStateAction,
6+
useState,
7+
} from "react";
8+
9+
import CollapsibleComponent from ".";
310

411
/*
512
* CHECKLIST
@@ -11,49 +18,72 @@ import ButtonPrimitive from '~/core/primitives/Button';
1118
*
1219
* */
1320

14-
export type CollapsibleProps = { open?: boolean, title?: string, trigger?: ReactNode} & PropsWithChildren;
15-
16-
const ExpandIcon = () => (
17-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className='size-6'>
18-
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
19-
</svg>
20-
);
21-
22-
const CollapseIcon = () => (
23-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className='size-6'>
24-
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25" />
25-
</svg>
26-
);
27-
28-
const Collapsible = ({ children, title, trigger, ...props }: CollapsibleProps) => {
29-
const [open, setOpen] = useState(props.open ?? true);
30-
31-
const toggleCollapse = () => setOpen((p) => !p);
32-
33-
return (
34-
<article>
35-
<span style={{ display: 'flex', alignItems: 'center' }}>
36-
{title && <p>{title}</p>}
37-
{
38-
trigger ||
39-
<ButtonPrimitive style={{ marginInlineStart: 'auto' }} onClick={toggleCollapse}>{open ? <CollapseIcon/> : <ExpandIcon/>}</ButtonPrimitive>
40-
}
41-
</span>
42-
43-
<div
44-
aria-hidden={!open}
45-
style={{
46-
overflow: 'hidden',
47-
display: 'flex',
48-
flexDirection: 'column',
49-
height: (props.open ?? open) ? 'auto' : '0',
50-
transition: 'all'
51-
}}>
52-
{children}
53-
</div>
54-
55-
</article>
56-
);
21+
export type CollapsibleProps = {
22+
open?: boolean;
23+
title?: string;
24+
trigger?: ReactNode;
25+
26+
disabled?: boolean;
27+
collapsibleContent?: ReactNode;
28+
29+
30+
onOpenChange?: Dispatch<SetStateAction<boolean>>;
31+
} & PropsWithChildren;
32+
33+
const Collapsible = ({ children, ...props }: CollapsibleProps) => {
34+
//State values if not provided by the user
35+
const [open, onOpenChange] = useState(props.open ?? false);
36+
37+
38+
// Disable or enable collapse
39+
const disabled = props.disabled ?? false;
40+
41+
// Title for the component
42+
const title = props.title;
43+
44+
// Collapsible Content
45+
const collapsibleContent = props.collapsibleContent;
46+
47+
return (
48+
<CollapsibleComponent.Root
49+
open={props.open ?? open}
50+
onOpenChange={props.onOpenChange ?? onOpenChange}
51+
disabled={disabled}
52+
>
53+
<CollapsibleComponent.Header title={title}>
54+
{/* Button */}
55+
56+
<CollapsibleComponent.Trigger asChild>
57+
{props.trigger && props.trigger}
58+
</CollapsibleComponent.Trigger>
59+
60+
</CollapsibleComponent.Header>
61+
62+
{/* Conditonal Loop */}
63+
{disabled ? (
64+
// loops through all the items with no toggle
65+
<>{children && children}
66+
{collapsibleContent && collapsibleContent}</>
67+
) : (
68+
<>
69+
{/* Default Content */}
70+
{children && children}
71+
{/* Collapsable Content */}
72+
<CollapsibleComponent.Content>
73+
{collapsibleContent && collapsibleContent}
74+
</CollapsibleComponent.Content>
75+
</>
76+
)}
77+
</CollapsibleComponent.Root>
78+
);
5779
};
5880

81+
82+
83+
Collapsible.Root = CollapsibleComponent.Root;
84+
Collapsible.Header = CollapsibleComponent.Header;
85+
Collapsible.Trigger = CollapsibleComponent.Trigger;
86+
Collapsible.Content = CollapsibleComponent.Content;
87+
Collapsible.Item = CollapsibleComponent.Item;
88+
5989
export default Collapsible;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createContext, Dispatch, SetStateAction } from "react";
2+
3+
type CollapsibleContextType = {
4+
rootClass?: string;
5+
open?: boolean;
6+
onOpenChange?: Dispatch<SetStateAction<boolean>>;
7+
disabled?: boolean;
8+
defaultOpen?: boolean;
9+
};
10+
11+
const defaultContext: CollapsibleContextType = {
12+
rootClass: "",
13+
open: false,
14+
onOpenChange: () => {},
15+
disabled: false,
16+
defaultOpen: false,
17+
};
18+
19+
export const CollapsibleContext =
20+
createContext<CollapsibleContextType>(defaultContext);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import clsx from 'clsx';
2+
import React, { useContext } from 'react';
3+
import { CollapsibleContext } from '../contexts/CollapsibleContext';
4+
5+
type CollapsibleContentProps = {
6+
children: React.ReactNode;
7+
className?: string;
8+
9+
}
10+
11+
const CollapsibleContent = ({children,className=''}:CollapsibleContentProps) => {
12+
13+
const {rootClass,open} = useContext(CollapsibleContext)
14+
15+
return (
16+
<div
17+
className={clsx(`${rootClass}-content`, className)}
18+
aria-hidden={!open}
19+
data-state={open? "expanded" : "collapsed"}
20+
>
21+
{open && children}
22+
</div>
23+
);
24+
}
25+
26+
export default CollapsibleContent
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import clsx from 'clsx'
2+
import React, { useContext } from 'react'
3+
import { CollapsibleContext } from '../contexts/CollapsibleContext'
4+
5+
type CollapsibleHeaderProps = {
6+
children?: React.ReactNode
7+
className?: string,
8+
title?: string
9+
}
10+
11+
const CollapsibleHeader = ({children,className="",title=""}: CollapsibleHeaderProps) => {
12+
const { rootClass } = useContext(CollapsibleContext)
13+
return (
14+
<div className={clsx(`${rootClass}-header`,className)}>{title && <p>{title}</p>}{children}</div>
15+
)
16+
}
17+
18+
export default CollapsibleHeader
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import clsx from "clsx";
2+
import React, { useContext } from "react";
3+
import { CollapsibleContext } from "../contexts/CollapsibleContext";
4+
5+
type CollapsibleItemProps = {
6+
children: React.ReactNode;
7+
className?: string;
8+
9+
};
10+
11+
const CollapsibleItem = ({
12+
children,
13+
className = "",
14+
15+
}: CollapsibleItemProps) => {
16+
const { rootClass } = useContext(CollapsibleContext);
17+
18+
return (
19+
<div className={clsx(`${rootClass}-item`, className)} >{children}</div>
20+
);
21+
};
22+
23+
export default CollapsibleItem;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import clsx from 'clsx';
2+
import React, { Dispatch, SetStateAction } from 'react';
3+
import { customClassSwitcher } from '~/core';
4+
import { CollapsibleContext } from '../contexts/CollapsibleContext';
5+
6+
7+
8+
9+
const COMPONENT_NAME = 'Collapsible'
10+
11+
export type CollapsibleRootProps = {
12+
children: React.ReactNode;
13+
customRootClass?: string;
14+
open: boolean;
15+
onOpenChange: Dispatch<SetStateAction<boolean>>;
16+
className?: string
17+
disabled?: boolean
18+
};
19+
20+
const CollapsibleRoot = ({children,className="",disabled, customRootClass, open, onOpenChange}: CollapsibleRootProps) => {
21+
22+
const rootClass = customClassSwitcher(customRootClass,COMPONENT_NAME)
23+
24+
return (
25+
<CollapsibleContext.Provider
26+
value={{
27+
rootClass,
28+
open,
29+
onOpenChange,
30+
disabled
31+
}}
32+
><div className={clsx(`${rootClass}-root`,className)}>
33+
{children}</div></CollapsibleContext.Provider>
34+
)
35+
}
36+
37+
export default CollapsibleRoot
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import clsx from 'clsx'
2+
import React, { useContext } from 'react'
3+
import Primitive from '~/core/primitives/Primitive'
4+
import { CollapsibleContext } from '../contexts/CollapsibleContext'
5+
6+
7+
type CollapsibleTriggerProps = {
8+
children?: React.ReactNode
9+
asChild?: boolean
10+
className?: string
11+
id?: string
12+
key?: string
13+
style?: React.CSSProperties
14+
index?: number
15+
}
16+
17+
18+
const CollapsibleTrigger = ({children,className, ...props}: CollapsibleTriggerProps) => {
19+
const {rootClass,open,onOpenChange,disabled} = useContext(CollapsibleContext)
20+
21+
const toggleCollapse = () => onOpenChange && !disabled && onOpenChange((p) => (!p))
22+
return (
23+
<Primitive.button
24+
className={clsx(`${rootClass}-trigger`, className)}
25+
role={"button"}
26+
tabIndex={0}
27+
onKeyDown={(e) => {
28+
if (e.key === "Enter" || e.key === " ") {
29+
e.preventDefault();
30+
toggleCollapse();
31+
}
32+
}}
33+
aria-expanded={open}
34+
onClick={toggleCollapse}
35+
{...props}
36+
37+
>
38+
{children}
39+
</Primitive.button>
40+
);
41+
}
42+
43+
export default CollapsibleTrigger
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import CollapsibleContent from "./fragments/CollapsibleContent";
2+
import CollapsibleHeader from "./fragments/CollapsibleHeader";
3+
import CollapsibleItem from "./fragments/CollapsibleItem";
4+
import CollapsibleRoot from "./fragments/CollapsibleRoot";
5+
import CollapsibleTrigger from "./fragments/CollapsibleTrigger";
6+
7+
8+
const CollapsibleComponent = {
9+
Root: CollapsibleRoot,
10+
Header: CollapsibleHeader,
11+
Trigger: CollapsibleTrigger,
12+
Content: CollapsibleContent,
13+
Item: CollapsibleItem,
14+
} as const;
15+
16+
export default CollapsibleComponent;

0 commit comments

Comments
 (0)