@seed-design/react-collapsible 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/Collapsible-12s-B5Klt2JP.cjs +156 -0
- package/lib/Collapsible-12s-CzE3seHv.js +151 -0
- package/lib/index.cjs +16 -0
- package/lib/index.d.ts +1173 -0
- package/lib/index.js +11 -0
- package/package.json +48 -0
- package/src/Collapsible.namespace.ts +8 -0
- package/src/Collapsible.tsx +69 -0
- package/src/dom.ts +1 -0
- package/src/index.ts +22 -0
- package/src/useCollapsible.test.tsx +203 -0
- package/src/useCollapsible.ts +121 -0
- package/src/useCollapsibleContext.tsx +21 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { C as CollapsibleContent, a as CollapsibleRoot, b as CollapsibleTrigger } from './Collapsible-12s-CzE3seHv.js';
|
|
2
|
+
export { c as CollapsibleProvider, d as useCollapsible, u as useCollapsibleContext } from './Collapsible-12s-CzE3seHv.js';
|
|
3
|
+
|
|
4
|
+
var Collapsible_namespace = {
|
|
5
|
+
__proto__: null,
|
|
6
|
+
Content: CollapsibleContent,
|
|
7
|
+
Root: CollapsibleRoot,
|
|
8
|
+
Trigger: CollapsibleTrigger
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export { Collapsible_namespace as Collapsible, CollapsibleContent, CollapsibleRoot, CollapsibleTrigger };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seed-design/react-collapsible",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/daangn/seed-design.git",
|
|
7
|
+
"directory": "packages/react-headless/collapsible"
|
|
8
|
+
},
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"import": "./lib/index.js",
|
|
15
|
+
"require": "./lib/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"main": "./lib/index.cjs",
|
|
20
|
+
"files": [
|
|
21
|
+
"lib",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "rm -rf lib",
|
|
26
|
+
"build": "bunchee",
|
|
27
|
+
"lint:publish": "bun publint"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@radix-ui/react-compose-refs": "^1.1.2",
|
|
31
|
+
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
|
32
|
+
"@radix-ui/react-use-layout-effect": "^1.1.1",
|
|
33
|
+
"@seed-design/dom-utils": "1.0.0",
|
|
34
|
+
"@seed-design/react-primitive": "1.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^19.1.6",
|
|
38
|
+
"react": "^19.1.0",
|
|
39
|
+
"react-dom": "^19.1.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": ">=18.0.0",
|
|
43
|
+
"react-dom": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { composeRefs } from "@radix-ui/react-compose-refs";
|
|
4
|
+
import { mergeProps } from "@seed-design/dom-utils";
|
|
5
|
+
import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
|
|
6
|
+
import type * as React from "react";
|
|
7
|
+
import { forwardRef } from "react";
|
|
8
|
+
import type { UseCollapsibleProps } from "./useCollapsible";
|
|
9
|
+
import { useCollapsible } from "./useCollapsible";
|
|
10
|
+
import { CollapsibleProvider, useCollapsibleContext } from "./useCollapsibleContext";
|
|
11
|
+
|
|
12
|
+
export interface CollapsibleRootProps
|
|
13
|
+
extends UseCollapsibleProps,
|
|
14
|
+
PrimitiveProps,
|
|
15
|
+
Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue"> {}
|
|
16
|
+
|
|
17
|
+
export const CollapsibleRoot = forwardRef<HTMLDivElement, CollapsibleRootProps>((props, ref) => {
|
|
18
|
+
const { open, defaultOpen, onOpenChange, disabled, ...otherProps } = props;
|
|
19
|
+
|
|
20
|
+
const api = useCollapsible({
|
|
21
|
+
open,
|
|
22
|
+
defaultOpen,
|
|
23
|
+
onOpenChange,
|
|
24
|
+
disabled,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<CollapsibleProvider value={api}>
|
|
29
|
+
<Primitive.div ref={ref} {...mergeProps(api.stateProps, otherProps)} />
|
|
30
|
+
</CollapsibleProvider>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
CollapsibleRoot.displayName = "CollapsibleRoot";
|
|
34
|
+
|
|
35
|
+
export interface CollapsibleTriggerProps
|
|
36
|
+
extends PrimitiveProps,
|
|
37
|
+
React.HTMLAttributes<HTMLButtonElement> {}
|
|
38
|
+
|
|
39
|
+
export const CollapsibleTrigger = forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
|
|
40
|
+
(props, ref) => {
|
|
41
|
+
const api = useCollapsibleContext();
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Primitive.button
|
|
45
|
+
ref={ref}
|
|
46
|
+
{...mergeProps(api.stateProps, api.triggerAriaProps, api.triggerHandlers, props)}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
CollapsibleTrigger.displayName = "CollapsibleTrigger";
|
|
52
|
+
|
|
53
|
+
export interface CollapsibleContentProps
|
|
54
|
+
extends PrimitiveProps,
|
|
55
|
+
React.HTMLAttributes<HTMLDivElement> {}
|
|
56
|
+
|
|
57
|
+
export const CollapsibleContent = forwardRef<HTMLDivElement, CollapsibleContentProps>(
|
|
58
|
+
(props, ref) => {
|
|
59
|
+
const api = useCollapsibleContext();
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Primitive.div
|
|
63
|
+
ref={composeRefs(ref, api.refs.content)}
|
|
64
|
+
{...mergeProps(api.contentProps, props)}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
CollapsibleContent.displayName = "CollapsibleContent";
|
package/src/dom.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const getContentId = (id: string) => `collapsible:${id}:content`;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CollapsibleRoot,
|
|
3
|
+
CollapsibleTrigger,
|
|
4
|
+
CollapsibleContent,
|
|
5
|
+
type CollapsibleRootProps,
|
|
6
|
+
type CollapsibleTriggerProps,
|
|
7
|
+
type CollapsibleContentProps,
|
|
8
|
+
} from "./Collapsible";
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
useCollapsibleContext,
|
|
12
|
+
CollapsibleProvider,
|
|
13
|
+
type UseCollapsibleContext,
|
|
14
|
+
} from "./useCollapsibleContext";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
useCollapsible,
|
|
18
|
+
type UseCollapsibleProps,
|
|
19
|
+
type UseCollapsibleReturn,
|
|
20
|
+
} from "./useCollapsible";
|
|
21
|
+
|
|
22
|
+
export * as Collapsible from "./Collapsible.namespace";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
|
2
|
+
import { cleanup, render } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CollapsibleContent,
|
|
11
|
+
CollapsibleRoot,
|
|
12
|
+
CollapsibleTrigger,
|
|
13
|
+
type CollapsibleRootProps,
|
|
14
|
+
} from "./Collapsible";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @see https://github.com/ZeeCoder/use-resize-observer/issues/40#issuecomment-644536259
|
|
18
|
+
* useCollapsible에서 사용하는 ResizeObserver를 mock으로 대체합니다.
|
|
19
|
+
*/
|
|
20
|
+
class ResizeObserver {
|
|
21
|
+
observe() {}
|
|
22
|
+
unobserve() {}
|
|
23
|
+
disconnect() {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
afterEach(cleanup);
|
|
27
|
+
|
|
28
|
+
function setUp(jsx: ReactElement) {
|
|
29
|
+
return {
|
|
30
|
+
user: userEvent.setup(),
|
|
31
|
+
...render(jsx),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Collapsible(props: CollapsibleRootProps) {
|
|
36
|
+
return (
|
|
37
|
+
<CollapsibleRoot {...props}>
|
|
38
|
+
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
|
39
|
+
<CollapsibleContent>Content</CollapsibleContent>
|
|
40
|
+
</CollapsibleRoot>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("useCollapsible", () => {
|
|
45
|
+
window.ResizeObserver = ResizeObserver;
|
|
46
|
+
|
|
47
|
+
it("should render the collapsible correctly", () => {
|
|
48
|
+
const { getByRole, getByText } = setUp(<Collapsible />);
|
|
49
|
+
|
|
50
|
+
const trigger = getByRole("button");
|
|
51
|
+
const content = getByText("Content");
|
|
52
|
+
|
|
53
|
+
expect(trigger).toBeInTheDocument();
|
|
54
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
55
|
+
expect(content).toHaveAttribute("hidden");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should render the collapsible with defaultOpen=true", () => {
|
|
59
|
+
const { getByRole, getByText } = setUp(<Collapsible defaultOpen={true} />);
|
|
60
|
+
|
|
61
|
+
const trigger = getByRole("button");
|
|
62
|
+
const content = getByText("Content");
|
|
63
|
+
|
|
64
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
65
|
+
expect(content).not.toHaveAttribute("hidden");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should render the collapsible with defaultOpen=false", () => {
|
|
69
|
+
const { getByRole, getByText } = setUp(<Collapsible defaultOpen={false} />);
|
|
70
|
+
|
|
71
|
+
const trigger = getByRole("button");
|
|
72
|
+
const content = getByText("Content");
|
|
73
|
+
|
|
74
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
75
|
+
expect(content).toHaveAttribute("hidden");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should toggle open state when trigger is clicked", async () => {
|
|
79
|
+
const { getByRole, getByText, user } = setUp(<Collapsible />);
|
|
80
|
+
|
|
81
|
+
const trigger = getByRole("button");
|
|
82
|
+
const content = getByText("Content");
|
|
83
|
+
|
|
84
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
85
|
+
expect(content).toHaveAttribute("hidden");
|
|
86
|
+
|
|
87
|
+
await user.click(trigger);
|
|
88
|
+
|
|
89
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
90
|
+
expect(content).not.toHaveAttribute("hidden");
|
|
91
|
+
|
|
92
|
+
await user.click(trigger);
|
|
93
|
+
|
|
94
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should call onOpenChange when toggle is clicked", async () => {
|
|
98
|
+
const handleOpenChange = vi.fn();
|
|
99
|
+
|
|
100
|
+
const { getByRole, user } = setUp(<Collapsible onOpenChange={handleOpenChange} />);
|
|
101
|
+
const trigger = getByRole("button");
|
|
102
|
+
|
|
103
|
+
await user.click(trigger);
|
|
104
|
+
expect(handleOpenChange).toHaveBeenCalledWith(true);
|
|
105
|
+
|
|
106
|
+
await user.click(trigger);
|
|
107
|
+
expect(handleOpenChange).toHaveBeenCalledWith(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should have correct aria-controls attribute", () => {
|
|
111
|
+
const { getByRole, getByText } = setUp(<Collapsible />);
|
|
112
|
+
|
|
113
|
+
const trigger = getByRole("button");
|
|
114
|
+
const content = getByText("Content");
|
|
115
|
+
|
|
116
|
+
const contentId = content.getAttribute("id");
|
|
117
|
+
expect(trigger).toHaveAttribute("aria-controls", contentId);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should have data-open attribute when open", async () => {
|
|
121
|
+
const { getByRole, user } = setUp(<Collapsible />);
|
|
122
|
+
|
|
123
|
+
const trigger = getByRole("button");
|
|
124
|
+
|
|
125
|
+
expect(trigger).not.toHaveAttribute("data-open");
|
|
126
|
+
|
|
127
|
+
await user.click(trigger);
|
|
128
|
+
|
|
129
|
+
expect(trigger).toHaveAttribute("data-open");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("disabled prop test", () => {
|
|
133
|
+
it("should have aria-disabled when disabled prop is true", () => {
|
|
134
|
+
const { getByRole } = setUp(<Collapsible disabled={true} />);
|
|
135
|
+
const trigger = getByRole("button");
|
|
136
|
+
|
|
137
|
+
expect(trigger).toHaveAttribute("aria-disabled", "true");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should not toggle when trigger is clicked while disabled", async () => {
|
|
141
|
+
const { getByRole, user } = setUp(<Collapsible disabled={true} />);
|
|
142
|
+
const trigger = getByRole("button");
|
|
143
|
+
|
|
144
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
145
|
+
|
|
146
|
+
await user.click(trigger);
|
|
147
|
+
|
|
148
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should not call onOpenChange when clicked while disabled", async () => {
|
|
152
|
+
const handleOpenChange = vi.fn();
|
|
153
|
+
|
|
154
|
+
const { getByRole, user } = setUp(
|
|
155
|
+
<Collapsible disabled={true} onOpenChange={handleOpenChange} />,
|
|
156
|
+
);
|
|
157
|
+
const trigger = getByRole("button");
|
|
158
|
+
|
|
159
|
+
await user.click(trigger);
|
|
160
|
+
expect(handleOpenChange).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should have data-disabled attribute when disabled", () => {
|
|
164
|
+
const { getByRole } = setUp(<Collapsible disabled={true} />);
|
|
165
|
+
const trigger = getByRole("button");
|
|
166
|
+
|
|
167
|
+
expect(trigger).toHaveAttribute("data-disabled");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("controlled mode", () => {
|
|
172
|
+
function ControlledCollapsible() {
|
|
173
|
+
const [open, setOpen] = React.useState(false);
|
|
174
|
+
return (
|
|
175
|
+
<div>
|
|
176
|
+
<button type="button" onClick={() => setOpen(true)}>
|
|
177
|
+
Open
|
|
178
|
+
</button>
|
|
179
|
+
<button type="button" onClick={() => setOpen(false)}>
|
|
180
|
+
Close
|
|
181
|
+
</button>
|
|
182
|
+
<Collapsible open={open} onOpenChange={setOpen} />
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
it("should be controlled by external state", async () => {
|
|
188
|
+
const { getByRole, getByText, user } = setUp(<ControlledCollapsible />);
|
|
189
|
+
|
|
190
|
+
const trigger = getByRole("button", { name: "Toggle" });
|
|
191
|
+
const openButton = getByText("Open");
|
|
192
|
+
const closeButton = getByText("Close");
|
|
193
|
+
|
|
194
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
195
|
+
|
|
196
|
+
await user.click(openButton);
|
|
197
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
198
|
+
|
|
199
|
+
await user.click(closeButton);
|
|
200
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
2
|
+
import { useLayoutEffect } from "@radix-ui/react-use-layout-effect";
|
|
3
|
+
import { dataAttr, elementProps } from "@seed-design/dom-utils";
|
|
4
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
|
+
import * as dom from "./dom";
|
|
6
|
+
|
|
7
|
+
export interface UseCollapsibleStateProps {
|
|
8
|
+
open?: boolean;
|
|
9
|
+
defaultOpen?: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useCollapsibleState(props: UseCollapsibleStateProps) {
|
|
14
|
+
const [open, setOpen] = useControllableState({
|
|
15
|
+
prop: props.open,
|
|
16
|
+
defaultProp: props.defaultOpen ?? false,
|
|
17
|
+
onChange: props.onOpenChange,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return useMemo(() => ({ open, setOpen }), [open, setOpen]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseCollapsibleProps extends UseCollapsibleStateProps {
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type UseCollapsibleReturn = ReturnType<typeof useCollapsible>;
|
|
28
|
+
|
|
29
|
+
export function useCollapsible(props: UseCollapsibleProps) {
|
|
30
|
+
const { open, setOpen } = useCollapsibleState(props);
|
|
31
|
+
const { disabled } = props;
|
|
32
|
+
|
|
33
|
+
const id = useId();
|
|
34
|
+
const contentId = dom.getContentId(id);
|
|
35
|
+
|
|
36
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
const [height, setHeight] = useState<number | undefined>(undefined);
|
|
38
|
+
const [visible, setVisible] = useState(open);
|
|
39
|
+
|
|
40
|
+
const hidden = !open && !visible;
|
|
41
|
+
|
|
42
|
+
useLayoutEffect(() => {
|
|
43
|
+
if (!contentRef.current) return;
|
|
44
|
+
|
|
45
|
+
const updateHeight = () => {
|
|
46
|
+
if (!contentRef.current) return;
|
|
47
|
+
|
|
48
|
+
setHeight(contentRef.current.offsetHeight);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
updateHeight();
|
|
52
|
+
|
|
53
|
+
const observer = new ResizeObserver(updateHeight);
|
|
54
|
+
observer.observe(contentRef.current);
|
|
55
|
+
|
|
56
|
+
return () => observer.disconnect();
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open) return;
|
|
61
|
+
|
|
62
|
+
// When expanded, immediately show to allow transition
|
|
63
|
+
setVisible(true);
|
|
64
|
+
}, [open]);
|
|
65
|
+
|
|
66
|
+
const panelHeight = open ? `${height}px` : "0px";
|
|
67
|
+
|
|
68
|
+
const stateProps = useMemo(
|
|
69
|
+
() =>
|
|
70
|
+
elementProps({
|
|
71
|
+
"data-collapsible": "",
|
|
72
|
+
"data-open": dataAttr(open),
|
|
73
|
+
"data-disabled": dataAttr(disabled),
|
|
74
|
+
}),
|
|
75
|
+
[open, disabled],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return useMemo(
|
|
79
|
+
() => ({
|
|
80
|
+
open,
|
|
81
|
+
setOpen,
|
|
82
|
+
disabled,
|
|
83
|
+
|
|
84
|
+
stateProps,
|
|
85
|
+
|
|
86
|
+
triggerAriaProps: elementProps({
|
|
87
|
+
"aria-expanded": open,
|
|
88
|
+
"aria-controls": contentId,
|
|
89
|
+
"aria-disabled": disabled,
|
|
90
|
+
}),
|
|
91
|
+
triggerHandlers: elementProps({
|
|
92
|
+
onClick: (event) => {
|
|
93
|
+
if (event.defaultPrevented) return;
|
|
94
|
+
if (disabled) return;
|
|
95
|
+
|
|
96
|
+
setOpen((prev) => !prev);
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
contentProps: elementProps({
|
|
101
|
+
...stateProps,
|
|
102
|
+
id: contentId,
|
|
103
|
+
hidden,
|
|
104
|
+
style: {
|
|
105
|
+
"--collapsible-content-height": height !== undefined ? panelHeight : undefined,
|
|
106
|
+
} as React.CSSProperties,
|
|
107
|
+
onTransitionEnd: (event) => {
|
|
108
|
+
if (event.propertyName !== "height") return;
|
|
109
|
+
if (open) return;
|
|
110
|
+
|
|
111
|
+
setVisible(false);
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
refs: {
|
|
116
|
+
content: contentRef,
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
[open, setOpen, disabled, stateProps, contentId, hidden, height, panelHeight],
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { UseCollapsibleReturn } from "./useCollapsible";
|
|
3
|
+
|
|
4
|
+
export interface UseCollapsibleContext extends UseCollapsibleReturn {}
|
|
5
|
+
|
|
6
|
+
const CollapsibleContext = createContext<UseCollapsibleContext | null>(null);
|
|
7
|
+
|
|
8
|
+
export const CollapsibleProvider = CollapsibleContext.Provider;
|
|
9
|
+
|
|
10
|
+
export function useCollapsibleContext<T extends boolean | undefined = true>({
|
|
11
|
+
strict = true,
|
|
12
|
+
}: {
|
|
13
|
+
strict?: T;
|
|
14
|
+
} = {}): T extends false ? UseCollapsibleContext | null : UseCollapsibleContext {
|
|
15
|
+
const context = useContext(CollapsibleContext);
|
|
16
|
+
if (!context && strict) {
|
|
17
|
+
throw new Error("useCollapsibleContext must be used within a CollapsibleRoot");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return context as UseCollapsibleContext;
|
|
21
|
+
}
|