@sproutsocial/seeds-react-collapsible 1.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/.eslintignore +6 -0
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +7 -0
- package/__tests__/features.test.tsx +73 -0
- package/__tests__/types.test.tsx +59 -0
- package/dist/esm/index.js +167 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/jest.config.js +9 -0
- package/package.json +41 -0
- package/src/Collapsible.stories.tsx +165 -0
- package/src/Collapsible.tsx +136 -0
- package/src/CollapsibleTypes.ts +13 -0
- package/src/index.ts +5 -0
- package/src/styles.ts +37 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +12 -0
package/.eslintignore
ADDED
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
yarn run v1.22.22
|
|
2
|
+
$ tsup --dts
|
|
3
|
+
CLI Building entry: src/index.ts
|
|
4
|
+
CLI Using tsconfig: tsconfig.json
|
|
5
|
+
CLI tsup v8.0.2
|
|
6
|
+
CLI Using tsup config: /home/runner/work/seeds/seeds/seeds-react/seeds-react-collapsible/tsup.config.ts
|
|
7
|
+
CLI Target: es2022
|
|
8
|
+
CLI Cleaning output folder
|
|
9
|
+
CJS Build start
|
|
10
|
+
ESM Build start
|
|
11
|
+
ESM dist/esm/index.js 4.68 KB
|
|
12
|
+
ESM dist/esm/index.js.map 17.91 KB
|
|
13
|
+
ESM ⚡️ Build success in 223ms
|
|
14
|
+
CJS dist/index.js 6.58 KB
|
|
15
|
+
CJS dist/index.js.map 17.90 KB
|
|
16
|
+
CJS ⚡️ Build success in 254ms
|
|
17
|
+
DTS Build start
|
|
18
|
+
DTS ⚡️ Build success in 15193ms
|
|
19
|
+
DTS dist/index.d.ts 1.03 KB
|
|
20
|
+
DTS dist/index.d.mts 1.03 KB
|
|
21
|
+
Done in 18.99s.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
fireEvent,
|
|
6
|
+
screen,
|
|
7
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
8
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
9
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
10
|
+
import { Collapsible } from "../src";
|
|
11
|
+
|
|
12
|
+
export interface TypeStatefulCollapseTestProps {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
isOpen?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const StatefulCollapse = ({
|
|
18
|
+
isOpen = false,
|
|
19
|
+
children,
|
|
20
|
+
}: TypeStatefulCollapseTestProps) => {
|
|
21
|
+
const [open, setOpen] = useState(isOpen);
|
|
22
|
+
|
|
23
|
+
const toggle = () => setOpen(!open);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Collapsible isOpen={open}>
|
|
27
|
+
<Collapsible.Trigger>
|
|
28
|
+
<Button appearance="secondary" onClick={toggle}>
|
|
29
|
+
{open ? "Hide" : "Show"}
|
|
30
|
+
</Button>
|
|
31
|
+
</Collapsible.Trigger>
|
|
32
|
+
|
|
33
|
+
<Collapsible.Panel>{children}</Collapsible.Panel>
|
|
34
|
+
</Collapsible>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("Collapsible", () => {
|
|
39
|
+
it("should render properly", async () => {
|
|
40
|
+
const { container } = render(
|
|
41
|
+
<StatefulCollapse>
|
|
42
|
+
<Box />
|
|
43
|
+
</StatefulCollapse>
|
|
44
|
+
);
|
|
45
|
+
expect(container).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should open panel when clicking trigger", async () => {
|
|
49
|
+
render(<StatefulCollapse>Panel text</StatefulCollapse>);
|
|
50
|
+
const trigger = screen.queryByRole("button");
|
|
51
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
52
|
+
// TS thinks the trigger might be null even though the previous expect would have failed if it was
|
|
53
|
+
trigger && fireEvent.click(trigger);
|
|
54
|
+
const panel = screen.queryByText(/Panel text/);
|
|
55
|
+
expect(panel).toHaveAttribute("aria-hidden", "false");
|
|
56
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should render open when isOpen is true", async () => {
|
|
60
|
+
render(<StatefulCollapse isOpen>Panel text</StatefulCollapse>);
|
|
61
|
+
const trigger = screen.queryByRole("button");
|
|
62
|
+
const panel = screen.queryByText(/Panel text/);
|
|
63
|
+
expect(panel).toHaveAttribute("aria-hidden", "false");
|
|
64
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("trigger should be properly labelled", async () => {
|
|
68
|
+
render(<StatefulCollapse isOpen>Panel text</StatefulCollapse>);
|
|
69
|
+
const trigger = screen.queryByRole("button");
|
|
70
|
+
const panel = screen.queryByText(/Panel text/);
|
|
71
|
+
expect(trigger).toHaveAttribute("aria-controls", panel?.id);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render } from "@sproutsocial/seeds-react-testing-library";
|
|
3
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
4
|
+
import Button from "@sproutsocial/seeds-react-button";
|
|
5
|
+
import { Collapsible } from "../src";
|
|
6
|
+
|
|
7
|
+
const toggle = jest.fn();
|
|
8
|
+
|
|
9
|
+
describe.skip("Collapsible/types", () => {
|
|
10
|
+
it("should render valid props", () => {
|
|
11
|
+
const contentWithButton = (
|
|
12
|
+
<Box
|
|
13
|
+
width="100%"
|
|
14
|
+
height="200px"
|
|
15
|
+
bg="container.background.base"
|
|
16
|
+
p={400}
|
|
17
|
+
mt="100px"
|
|
18
|
+
>
|
|
19
|
+
<Button appearance="secondary">A button</Button>
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
const shortContent = (
|
|
23
|
+
<Box width="15%" height="50px" bg="container.background.base" p={400}>
|
|
24
|
+
Hello.
|
|
25
|
+
</Box>
|
|
26
|
+
);
|
|
27
|
+
render(
|
|
28
|
+
<>
|
|
29
|
+
<Collapsible isOpen={true} offset={0}>
|
|
30
|
+
<Collapsible.Trigger>
|
|
31
|
+
<Button appearance="secondary" onClick={toggle}>
|
|
32
|
+
Hide
|
|
33
|
+
</Button>
|
|
34
|
+
</Collapsible.Trigger>
|
|
35
|
+
<Collapsible.Panel>{contentWithButton}</Collapsible.Panel>
|
|
36
|
+
</Collapsible>
|
|
37
|
+
|
|
38
|
+
<Collapsible isOpen={false} offset={100}>
|
|
39
|
+
<Collapsible.Trigger>
|
|
40
|
+
<Button appearance="secondary" onClick={toggle}>
|
|
41
|
+
Show
|
|
42
|
+
</Button>
|
|
43
|
+
</Collapsible.Trigger>
|
|
44
|
+
<Collapsible.Panel>{contentWithButton}</Collapsible.Panel>
|
|
45
|
+
</Collapsible>
|
|
46
|
+
|
|
47
|
+
<Collapsible isOpen={false} collapsedHeight={500}>
|
|
48
|
+
<Collapsible.Panel>{shortContent}</Collapsible.Panel>
|
|
49
|
+
<Collapsible.Trigger>
|
|
50
|
+
<Button onClick={toggle}>Show More</Button>
|
|
51
|
+
</Collapsible.Trigger>
|
|
52
|
+
</Collapsible>
|
|
53
|
+
|
|
54
|
+
{/* @ts-expect-error - test missing required props is rejected */}
|
|
55
|
+
<Collapsible />
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/Collapsible.tsx
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { useState as useState2, useRef as useRef2, useContext, useEffect as useEffect2 } from "react";
|
|
4
|
+
|
|
5
|
+
// ../seeds-react-hooks/dist/index.mjs
|
|
6
|
+
import { useState, useLayoutEffect, useCallback, useReducer, useRef, useEffect, useMemo } from "react";
|
|
7
|
+
import { useTheme } from "styled-components";
|
|
8
|
+
var v = Object.freeze({ x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 });
|
|
9
|
+
function q(r) {
|
|
10
|
+
let [t, e] = useState(v);
|
|
11
|
+
return useLayoutEffect(() => {
|
|
12
|
+
if (!r.current || !("ResizeObserver" in window))
|
|
13
|
+
return;
|
|
14
|
+
let n = new ResizeObserver(([o]) => {
|
|
15
|
+
if (!o)
|
|
16
|
+
return;
|
|
17
|
+
let { x: u, y: a, width: i, height: f, top: m, right: b, bottom: d, left: y } = o.contentRect;
|
|
18
|
+
e({ x: u, y: a, width: i, height: f, top: m, right: b, bottom: d, left: y });
|
|
19
|
+
});
|
|
20
|
+
return n.observe(r.current), () => {
|
|
21
|
+
n.disconnect();
|
|
22
|
+
};
|
|
23
|
+
}, [r]), t;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/Collapsible.tsx
|
|
27
|
+
import Box2 from "@sproutsocial/seeds-react-box";
|
|
28
|
+
|
|
29
|
+
// src/styles.ts
|
|
30
|
+
import styled from "styled-components";
|
|
31
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
32
|
+
var CollapsingBox = styled(Box)`
|
|
33
|
+
transition: max-height ${(p) => p.theme.duration.medium}
|
|
34
|
+
${(p) => p.theme.easing.ease_inout};
|
|
35
|
+
will-change: max-height;
|
|
36
|
+
position: relative;
|
|
37
|
+
overflow: auto;
|
|
38
|
+
${({ hasShadow, scrollable }) => hasShadow ? `background: /* Shadow covers */ linear-gradient(
|
|
39
|
+
transparent 30%,
|
|
40
|
+
rgba(255, 255, 255, 0)
|
|
41
|
+
),
|
|
42
|
+
linear-gradient(rgba(255, 255, 255, 0), transparent 70%) 0 100%,
|
|
43
|
+
/* Shadows */
|
|
44
|
+
radial-gradient(
|
|
45
|
+
farthest-side at 50% 0,
|
|
46
|
+
rgb(39 51 51 / 5%),
|
|
47
|
+
rgba(0, 0, 0, 0)
|
|
48
|
+
),
|
|
49
|
+
radial-gradient(
|
|
50
|
+
farthest-side at 50% 100%,
|
|
51
|
+
rgb(39 51 51 / 5%),
|
|
52
|
+
rgba(0, 0, 0, 0)
|
|
53
|
+
)
|
|
54
|
+
0 100%;
|
|
55
|
+
background-repeat: no-repeat;
|
|
56
|
+
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
|
57
|
+
background-attachment: local, local, scroll, scroll;
|
|
58
|
+
${scrollable ? `overflow: auto` : `overflow: hidden`};` : ""}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
// src/Collapsible.tsx
|
|
62
|
+
import { jsx } from "react/jsx-runtime";
|
|
63
|
+
var idCounter = 0;
|
|
64
|
+
var CollapsibleContext = React.createContext({});
|
|
65
|
+
var Collapsible = ({
|
|
66
|
+
children,
|
|
67
|
+
isOpen = false,
|
|
68
|
+
offset = 0,
|
|
69
|
+
collapsedHeight = 0,
|
|
70
|
+
openHeight
|
|
71
|
+
}) => {
|
|
72
|
+
const [id] = useState2(`Racine-collapsible-${idCounter++}`);
|
|
73
|
+
return /* @__PURE__ */ jsx(
|
|
74
|
+
CollapsibleContext.Provider,
|
|
75
|
+
{
|
|
76
|
+
value: {
|
|
77
|
+
isOpen,
|
|
78
|
+
id,
|
|
79
|
+
offset,
|
|
80
|
+
collapsedHeight,
|
|
81
|
+
openHeight
|
|
82
|
+
},
|
|
83
|
+
children
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
var determineMaxHeight = (isHidden, openHeight, computedHeight) => {
|
|
88
|
+
if (isHidden === void 0)
|
|
89
|
+
return void 0;
|
|
90
|
+
if (openHeight)
|
|
91
|
+
return openHeight;
|
|
92
|
+
return computedHeight;
|
|
93
|
+
};
|
|
94
|
+
var Trigger = ({ children, ...rest }) => {
|
|
95
|
+
const { isOpen, id } = useContext(CollapsibleContext);
|
|
96
|
+
return /* @__PURE__ */ jsx(React.Fragment, { children: React.cloneElement(children, {
|
|
97
|
+
"aria-controls": id,
|
|
98
|
+
"aria-expanded": !!isOpen,
|
|
99
|
+
...rest
|
|
100
|
+
}) });
|
|
101
|
+
};
|
|
102
|
+
Trigger.displayName = "Collapsible.Trigger";
|
|
103
|
+
var Panel = ({ children, ...rest }) => {
|
|
104
|
+
const {
|
|
105
|
+
isOpen,
|
|
106
|
+
id,
|
|
107
|
+
offset = 0,
|
|
108
|
+
collapsedHeight,
|
|
109
|
+
openHeight
|
|
110
|
+
} = useContext(CollapsibleContext);
|
|
111
|
+
const ref = useRef2(null);
|
|
112
|
+
const measurement = q(ref);
|
|
113
|
+
const [isHidden, setIsHidden] = useState2(void 0);
|
|
114
|
+
const maxHeight = determineMaxHeight(
|
|
115
|
+
isHidden,
|
|
116
|
+
openHeight,
|
|
117
|
+
// Round up to the nearest pixel to prevent subpixel rendering issues
|
|
118
|
+
Math.ceil(measurement.height + offset)
|
|
119
|
+
);
|
|
120
|
+
useEffect2(() => {
|
|
121
|
+
if (!isOpen) {
|
|
122
|
+
const timeoutID = setTimeout(() => setIsHidden(!isOpen), 300);
|
|
123
|
+
return () => clearTimeout(timeoutID);
|
|
124
|
+
} else {
|
|
125
|
+
const timeoutID = setTimeout(() => setIsHidden(!isOpen), 0);
|
|
126
|
+
return () => clearTimeout(timeoutID);
|
|
127
|
+
}
|
|
128
|
+
}, [isOpen]);
|
|
129
|
+
return /* @__PURE__ */ jsx(
|
|
130
|
+
CollapsingBox,
|
|
131
|
+
{
|
|
132
|
+
hasShadow: Boolean(collapsedHeight || openHeight && openHeight > 0),
|
|
133
|
+
scrollable: isOpen,
|
|
134
|
+
maxHeight: isOpen ? maxHeight : collapsedHeight,
|
|
135
|
+
minHeight: collapsedHeight,
|
|
136
|
+
"data-qa-collapsible": "",
|
|
137
|
+
"data-qa-collapsible-isopen": isOpen === true,
|
|
138
|
+
...rest,
|
|
139
|
+
children: /* @__PURE__ */ jsx(
|
|
140
|
+
Box2,
|
|
141
|
+
{
|
|
142
|
+
width: "100%",
|
|
143
|
+
hidden: isHidden && collapsedHeight === 0,
|
|
144
|
+
"aria-hidden": !isOpen,
|
|
145
|
+
id,
|
|
146
|
+
ref,
|
|
147
|
+
children
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
Panel.displayName = "Collapsible.Panel";
|
|
154
|
+
Collapsible.Trigger = Trigger;
|
|
155
|
+
Collapsible.Panel = Panel;
|
|
156
|
+
var Collapsible_default = Collapsible;
|
|
157
|
+
|
|
158
|
+
// src/CollapsibleTypes.ts
|
|
159
|
+
import "react";
|
|
160
|
+
|
|
161
|
+
// src/index.ts
|
|
162
|
+
var src_default = Collapsible_default;
|
|
163
|
+
export {
|
|
164
|
+
Collapsible_default as Collapsible,
|
|
165
|
+
src_default as default
|
|
166
|
+
};
|
|
167
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Collapsible.tsx","../../../seeds-react-hooks/src/useMeasure/useMeasure.ts","../../../seeds-react-hooks/src/useSelect/useSelect.ts","../../../seeds-react-hooks/src/useMultiselect/useMultiselect.ts","../../../seeds-react-hooks/src/useMutationObserver/useMutationObserver.ts","../../../seeds-react-hooks/src/useTextContent/useTextContent.ts","../../../seeds-react-hooks/src/useWhyDidYouUpdate/useWhyDidYouUpdate.ts","../../../seeds-react-hooks/src/useInteractiveColor/useInteractiveColor.ts","../../src/styles.ts","../../src/CollapsibleTypes.ts","../../src/index.ts"],"sourcesContent":["import * as React from \"react\";\nimport { useState, useRef, useContext, useEffect } from \"react\";\nimport { useMeasure } from \"@sproutsocial/seeds-react-hooks\";\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport { CollapsingBox } from \"./styles\";\nimport type { TypeCollapsibleProps } from \"./CollapsibleTypes\";\n\nlet idCounter = 0;\n\ninterface TypeCollapsibleContext {\n isOpen?: boolean;\n id?: string;\n offset?: number;\n openHeight?: number;\n collapsedHeight?: number;\n}\n\nconst CollapsibleContext = React.createContext<TypeCollapsibleContext>({});\n\nconst Collapsible = ({\n children,\n isOpen = false,\n offset = 0,\n collapsedHeight = 0,\n openHeight,\n}: TypeCollapsibleProps) => {\n const [id] = useState(`Racine-collapsible-${idCounter++}`);\n return (\n <CollapsibleContext.Provider\n value={{\n isOpen,\n id,\n offset,\n collapsedHeight,\n openHeight,\n }}\n >\n {children}\n </CollapsibleContext.Provider>\n );\n};\n\nconst determineMaxHeight = (\n isHidden?: boolean,\n openHeight?: number,\n computedHeight?: number\n): number | undefined => {\n // If isHidden is undefined this is the first render. Return undefined so the max-height prop is not added\n // This is a hack to prevent css from animating if it begins in the open state\n // css animates when attribute values change (IE from 0 to another number)\n // css does not animate when simply adding an attribute to an HTML element\n if (isHidden === undefined) return undefined;\n // If the user has defined an explicit open height, return that as the max height\n if (openHeight) return openHeight;\n // Otherwise, fallback to the computed height\n return computedHeight;\n};\n\nconst Trigger = ({ children, ...rest }: { children: React.ReactElement }) => {\n const { isOpen, id } = useContext(CollapsibleContext);\n return (\n <React.Fragment>\n {React.cloneElement(children, {\n \"aria-controls\": id,\n \"aria-expanded\": !!isOpen,\n ...rest,\n })}\n </React.Fragment>\n );\n};\n\nTrigger.displayName = \"Collapsible.Trigger\";\n\nconst Panel = ({ children, ...rest }: { children: React.ReactNode }) => {\n const {\n isOpen,\n id,\n offset = 0,\n collapsedHeight,\n openHeight,\n } = useContext(CollapsibleContext);\n\n const ref = useRef<HTMLDivElement | null>(null);\n const measurement = useMeasure(ref);\n const [isHidden, setIsHidden] = useState<boolean | undefined>(undefined);\n const maxHeight = determineMaxHeight(\n isHidden,\n openHeight,\n // Round up to the nearest pixel to prevent subpixel rendering issues\n Math.ceil(measurement.height + offset)\n );\n\n /* We use the \"hidden\" attribute to remove the contents of the panel from the tab order of the page, but it interferes with the animation. This logic sets a slight timeout on setting the prop so that the animation has time to complete before the attribute is set. */\n useEffect(() => {\n if (!isOpen) {\n const timeoutID = setTimeout(() => setIsHidden(!isOpen), 300);\n return () => clearTimeout(timeoutID);\n } else {\n // Similar to the close animation, we need to delay setting hidden to run slightly async.\n // An issue occurs with the initial render isHidden logic that causes the animation to occur sporadically.\n // using this 0 second timeout just allows this component to initially render with an undefined max height,\n // Then go directly from undefined to the full max height, without a brief 0 value that triggers an animation\n const timeoutID = setTimeout(() => setIsHidden(!isOpen), 0);\n return () => clearTimeout(timeoutID);\n }\n }, [isOpen]);\n\n return (\n <CollapsingBox\n hasShadow={Boolean(collapsedHeight || (openHeight && openHeight > 0))}\n scrollable={isOpen}\n maxHeight={isOpen ? maxHeight : collapsedHeight}\n minHeight={collapsedHeight}\n data-qa-collapsible=\"\"\n data-qa-collapsible-isopen={isOpen === true}\n {...rest}\n >\n <Box\n width=\"100%\"\n hidden={isHidden && collapsedHeight === 0}\n aria-hidden={!isOpen}\n id={id}\n ref={ref}\n >\n {children}\n </Box>\n </CollapsingBox>\n );\n};\n\nPanel.displayName = \"Collapsible.Panel\";\n\nCollapsible.Trigger = Trigger;\nCollapsible.Panel = Panel;\n\nexport default Collapsible;\n","import { useState, useLayoutEffect, type RefObject } from \"react\";\n\ninterface DOMRectObject {\n x: number;\n y: number;\n width: number;\n height: number;\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\nconst initialBounds = Object.freeze({\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n});\n\nexport function useMeasure<TElement extends Element>(ref: RefObject<TElement>) {\n const [bounds, setContentRect] =\n useState<Readonly<DOMRectObject>>(initialBounds);\n\n useLayoutEffect(() => {\n const element = ref.current;\n\n if (\n !element ||\n // in non-browser environments (e.g. Jest tests) ResizeObserver is not defined\n !(\"ResizeObserver\" in window)\n ) {\n return;\n }\n\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return;\n const { x, y, width, height, top, right, bottom, left } =\n entry.contentRect;\n setContentRect({\n x,\n y,\n width,\n height,\n top,\n right,\n bottom,\n left,\n });\n });\n resizeObserver.observe(ref.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [ref]);\n\n return bounds;\n}\n","import { useState, useCallback } from \"react\";\n\ntype TypeSingleSelectProps<T extends string> = {\n initialValue?: T | \"\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: string | T) => any;\n};\n\nexport const useSelect = <T extends string>(\n {\n initialValue = \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeSingleSelectProps<T> = {\n initialValue: \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, setValue] = useState<string | T>(initialValue);\n\n const onChange = useCallback(\n (newValue: string) => {\n if (newValue !== value) {\n setValue(newValue);\n userOnChange(newValue);\n }\n },\n [userOnChange, value]\n );\n\n return { value, onChange };\n};\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\ntype TypeMultiSelectProps<T extends string> = {\n initialValue?: T[];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: Array<string | T>) => any;\n};\n\nconst valueReducer = (\n state: Set<string>,\n action: { type: string; value?: string }\n): Set<string> => {\n const newState = new Set(state);\n switch (action.type) {\n case \"reset\": {\n return new Set();\n }\n case \"toggle_item\":\n default: {\n if (action.value) {\n if (newState.has(action.value)) {\n newState.delete(action.value);\n } else {\n newState.add(action.value);\n }\n }\n return newState;\n }\n }\n};\n\nexport const useMultiselect = <T extends string>(\n {\n initialValue = [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeMultiSelectProps<T> = {\n initialValue: [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, dispatch] = useReducer(valueReducer, new Set(initialValue));\n\n const getArrayValue = (value: Set<string | T>) =>\n Array.from<string | T>(value);\n\n const onChange = useCallback(\n (newValue: string) => {\n dispatch({ type: \"toggle_item\", value: newValue });\n },\n [dispatch]\n );\n\n const isFirstRun = useRef(true);\n\n useEffect(() => {\n if (isFirstRun.current) {\n isFirstRun.current = false;\n return;\n }\n userOnChange(getArrayValue(value));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [userOnChange, value]);\n\n const onClear = useCallback(() => {\n dispatch({ type: \"reset\" });\n }, [dispatch]);\n\n return { value: getArrayValue(value), onChange, onClear };\n};\n","import { canUseDOM } from \"@sproutsocial/seeds-react-utilities\";\nimport { useEffect, useMemo, useState } from \"react\";\n\ntype TypeMutationObserverInitRequired =\n | {\n childList: true;\n }\n | {\n attributes: true;\n }\n | {\n characterData: true;\n };\n\ntype TypeMutationObserverInit = {\n subtree?: boolean;\n attributeOldValue?: boolean;\n characterDataOldValue?: boolean;\n attributeFilter?: Array<string>;\n} & TypeMutationObserverInitRequired;\n\ntype TypeMutationObserverCallback = (\n mutationList?: MutationRecord[],\n observer?: MutationObserver\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n) => any;\n\nconst defaultCallback: TypeMutationObserverCallback = (mutationList) =>\n mutationList;\n\nexport function useMutationObserver(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback = defaultCallback\n) {\n if (!canUseDOM()) {\n return;\n }\n /* eslint-disable-next-line */\n const [value, setValue] = useState(undefined);\n /* eslint-disable-next-line */\n const observer = useMemo(\n () =>\n new MutationObserver((mutationList, observer) => {\n const result = callback(mutationList, observer);\n setValue(result);\n }),\n [callback]\n );\n /* eslint-disable-next-line */\n useEffect(() => {\n if (targetNode) {\n observer.observe(targetNode, config);\n return () => {\n observer.disconnect();\n };\n }\n }, [targetNode, config, observer]);\n\n return value;\n}\n\nexport function useMutationObserverOnce(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback\n) {\n const [isObserving, setObserving] = useState(true);\n const node = isObserving ? targetNode : null;\n const value = useMutationObserver(node, config, callback);\n if (value !== undefined && isObserving) {\n setObserving(false);\n }\n return value;\n}\n","import { useCallback, useState } from \"react\";\n\nexport type textContentRef = ((node: Node) => void) & { current?: string };\nexport function useTextContent(initial: string) {\n const [textContent, setTextContent] = useState(initial);\n\n const ref: textContentRef = useCallback((node: Node) => {\n if (node && node.textContent !== null) {\n setTextContent(node.textContent);\n }\n }, []);\n\n ref.current = textContent;\n return ref;\n}\n","import { useRef, useEffect } from \"react\";\n\nexport function useWhyDidYouUpdate(\n name: string,\n props: { [key: string]: any }\n) {\n // Get a mutable ref object where we can store props ...\n // ... for comparison next time this hook runs.\n const previousProps = useRef<typeof props>({});\n\n useEffect(() => {\n if (previousProps.current) {\n // Get all keys from previous and current props\n const allKeys = Object.keys({ ...previousProps.current, ...props });\n // Use this object to keep track of changed props\n const changesObj: typeof props = {};\n // Iterate through keys\n allKeys.forEach((key) => {\n // If previous is different from current\n\n if (previousProps.current[key] !== props[key]) {\n // Add to changesObj\n\n changesObj[key] = {\n from: previousProps.current[key],\n\n to: props[key],\n };\n }\n });\n\n // If changesObj not empty then output to console\n if (Object.keys(changesObj).length) {\n // eslint-disable-next-line no-console\n console.log(\"[why-did-you-update]\", name, changesObj);\n }\n }\n\n // Finally update previousProps with current props for next hook call\n previousProps.current = props;\n });\n}\n","import { darken, lighten } from \"polished\";\nimport { useTheme } from \"styled-components\";\nimport type { TypeTheme } from \"@sproutsocial/seeds-react-theme\";\n\n/**\n * The useInteractiveColor hook has context of theme mode (light or dark)\n * and can be used to lighten or darken a color dynamically\n *\n * note: colors are limited to our theme colors\n */\nconst useInteractiveColor = (themeColor: string): string => {\n // Throw error if used outside of a ThemeProvider (styled-components)\n if (!useTheme()) {\n throw new Error(\n \"useInteractiveColor() must be used within a Styled Components ThemeProvider\"\n );\n }\n\n // Get the current theme mode ie. 'light' or 'dark'\n const theme: TypeTheme = useTheme() as TypeTheme;\n const themeMode = theme.mode;\n\n // If the theme mode is dark, return a lightened version of the themeValue\n if (themeMode === \"dark\") {\n return lighten(0.2, themeColor);\n } else {\n // If the theme mode is light, return a darkened version of the themeValue\n return darken(0.2, themeColor);\n }\n};\n\nexport { useInteractiveColor };\n","import styled from \"styled-components\";\nimport Box from \"@sproutsocial/seeds-react-box\";\n\nexport const CollapsingBox = styled(Box)<{\n hasShadow?: boolean;\n scrollable?: boolean;\n}>`\n transition: max-height ${(p) => p.theme.duration.medium}\n ${(p) => p.theme.easing.ease_inout};\n will-change: max-height;\n position: relative;\n overflow: auto;\n ${({ hasShadow, scrollable }) =>\n hasShadow\n ? `background: /* Shadow covers */ linear-gradient(\n transparent 30%,\n rgba(255, 255, 255, 0)\n ),\n linear-gradient(rgba(255, 255, 255, 0), transparent 70%) 0 100%,\n /* Shadows */\n radial-gradient(\n farthest-side at 50% 0,\n rgb(39 51 51 / 5%),\n rgba(0, 0, 0, 0)\n ),\n radial-gradient(\n farthest-side at 50% 100%,\n rgb(39 51 51 / 5%),\n rgba(0, 0, 0, 0)\n )\n 0 100%;\n background-repeat: no-repeat;\n background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;\n background-attachment: local, local, scroll, scroll;\n ${scrollable ? `overflow: auto` : `overflow: hidden`};`\n : \"\"}\n`;\n","import * as React from \"react\";\n\n// The flow type is inexact but the underlying component does not accept any other props.\n// It might be worth extending the box props here for the refactor, but allowing it would provide no functionality right now.\nexport interface TypeCollapsibleProps {\n isOpen: boolean;\n children: React.ReactNode;\n\n /** If the children of the collapsible panel have a top or bottom margin, it will throw off the calculations for the height of the content. The total amount of vertical margin (in pixels) can be supplied to this prop to correct this. */\n offset?: number;\n collapsedHeight?: number;\n openHeight?: number;\n}\n","import Collapsible from \"./Collapsible\";\n\nexport default Collapsible;\nexport { Collapsible };\nexport * from \"./CollapsibleTypes\";\n"],"mappings":"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","names":["useState","useRef","useEffect","useState","Box","useState","useRef","useEffect","Box"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TypeCollapsibleProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/** If the children of the collapsible panel have a top or bottom margin, it will throw off the calculations for the height of the content. The total amount of vertical margin (in pixels) can be supplied to this prop to correct this. */
|
|
8
|
+
offset?: number;
|
|
9
|
+
collapsedHeight?: number;
|
|
10
|
+
openHeight?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare const Collapsible: {
|
|
14
|
+
({ children, isOpen, offset, collapsedHeight, openHeight, }: TypeCollapsibleProps): react_jsx_runtime.JSX.Element;
|
|
15
|
+
Trigger: {
|
|
16
|
+
({ children, ...rest }: {
|
|
17
|
+
children: React.ReactElement;
|
|
18
|
+
}): react_jsx_runtime.JSX.Element;
|
|
19
|
+
displayName: string;
|
|
20
|
+
};
|
|
21
|
+
Panel: {
|
|
22
|
+
({ children, ...rest }: {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}): react_jsx_runtime.JSX.Element;
|
|
25
|
+
displayName: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { Collapsible, type TypeCollapsibleProps, Collapsible as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
interface TypeCollapsibleProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/** If the children of the collapsible panel have a top or bottom margin, it will throw off the calculations for the height of the content. The total amount of vertical margin (in pixels) can be supplied to this prop to correct this. */
|
|
8
|
+
offset?: number;
|
|
9
|
+
collapsedHeight?: number;
|
|
10
|
+
openHeight?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare const Collapsible: {
|
|
14
|
+
({ children, isOpen, offset, collapsedHeight, openHeight, }: TypeCollapsibleProps): react_jsx_runtime.JSX.Element;
|
|
15
|
+
Trigger: {
|
|
16
|
+
({ children, ...rest }: {
|
|
17
|
+
children: React.ReactElement;
|
|
18
|
+
}): react_jsx_runtime.JSX.Element;
|
|
19
|
+
displayName: string;
|
|
20
|
+
};
|
|
21
|
+
Panel: {
|
|
22
|
+
({ children, ...rest }: {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}): react_jsx_runtime.JSX.Element;
|
|
25
|
+
displayName: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { Collapsible, type TypeCollapsibleProps, Collapsible as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Collapsible: () => Collapsible_default,
|
|
34
|
+
default: () => src_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
|
|
38
|
+
// src/Collapsible.tsx
|
|
39
|
+
var React = __toESM(require("react"));
|
|
40
|
+
var import_react2 = require("react");
|
|
41
|
+
|
|
42
|
+
// ../seeds-react-hooks/dist/index.mjs
|
|
43
|
+
var import_react = require("react");
|
|
44
|
+
var import_styled_components = require("styled-components");
|
|
45
|
+
var v = Object.freeze({ x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 });
|
|
46
|
+
function q(r) {
|
|
47
|
+
let [t, e] = (0, import_react.useState)(v);
|
|
48
|
+
return (0, import_react.useLayoutEffect)(() => {
|
|
49
|
+
if (!r.current || !("ResizeObserver" in window))
|
|
50
|
+
return;
|
|
51
|
+
let n = new ResizeObserver(([o]) => {
|
|
52
|
+
if (!o)
|
|
53
|
+
return;
|
|
54
|
+
let { x: u, y: a, width: i, height: f, top: m, right: b, bottom: d, left: y } = o.contentRect;
|
|
55
|
+
e({ x: u, y: a, width: i, height: f, top: m, right: b, bottom: d, left: y });
|
|
56
|
+
});
|
|
57
|
+
return n.observe(r.current), () => {
|
|
58
|
+
n.disconnect();
|
|
59
|
+
};
|
|
60
|
+
}, [r]), t;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/Collapsible.tsx
|
|
64
|
+
var import_seeds_react_box2 = __toESM(require("@sproutsocial/seeds-react-box"));
|
|
65
|
+
|
|
66
|
+
// src/styles.ts
|
|
67
|
+
var import_styled_components2 = __toESM(require("styled-components"));
|
|
68
|
+
var import_seeds_react_box = __toESM(require("@sproutsocial/seeds-react-box"));
|
|
69
|
+
var CollapsingBox = (0, import_styled_components2.default)(import_seeds_react_box.default)`
|
|
70
|
+
transition: max-height ${(p) => p.theme.duration.medium}
|
|
71
|
+
${(p) => p.theme.easing.ease_inout};
|
|
72
|
+
will-change: max-height;
|
|
73
|
+
position: relative;
|
|
74
|
+
overflow: auto;
|
|
75
|
+
${({ hasShadow, scrollable }) => hasShadow ? `background: /* Shadow covers */ linear-gradient(
|
|
76
|
+
transparent 30%,
|
|
77
|
+
rgba(255, 255, 255, 0)
|
|
78
|
+
),
|
|
79
|
+
linear-gradient(rgba(255, 255, 255, 0), transparent 70%) 0 100%,
|
|
80
|
+
/* Shadows */
|
|
81
|
+
radial-gradient(
|
|
82
|
+
farthest-side at 50% 0,
|
|
83
|
+
rgb(39 51 51 / 5%),
|
|
84
|
+
rgba(0, 0, 0, 0)
|
|
85
|
+
),
|
|
86
|
+
radial-gradient(
|
|
87
|
+
farthest-side at 50% 100%,
|
|
88
|
+
rgb(39 51 51 / 5%),
|
|
89
|
+
rgba(0, 0, 0, 0)
|
|
90
|
+
)
|
|
91
|
+
0 100%;
|
|
92
|
+
background-repeat: no-repeat;
|
|
93
|
+
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
|
94
|
+
background-attachment: local, local, scroll, scroll;
|
|
95
|
+
${scrollable ? `overflow: auto` : `overflow: hidden`};` : ""}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
// src/Collapsible.tsx
|
|
99
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
100
|
+
var idCounter = 0;
|
|
101
|
+
var CollapsibleContext = React.createContext({});
|
|
102
|
+
var Collapsible = ({
|
|
103
|
+
children,
|
|
104
|
+
isOpen = false,
|
|
105
|
+
offset = 0,
|
|
106
|
+
collapsedHeight = 0,
|
|
107
|
+
openHeight
|
|
108
|
+
}) => {
|
|
109
|
+
const [id] = (0, import_react2.useState)(`Racine-collapsible-${idCounter++}`);
|
|
110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
111
|
+
CollapsibleContext.Provider,
|
|
112
|
+
{
|
|
113
|
+
value: {
|
|
114
|
+
isOpen,
|
|
115
|
+
id,
|
|
116
|
+
offset,
|
|
117
|
+
collapsedHeight,
|
|
118
|
+
openHeight
|
|
119
|
+
},
|
|
120
|
+
children
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
var determineMaxHeight = (isHidden, openHeight, computedHeight) => {
|
|
125
|
+
if (isHidden === void 0)
|
|
126
|
+
return void 0;
|
|
127
|
+
if (openHeight)
|
|
128
|
+
return openHeight;
|
|
129
|
+
return computedHeight;
|
|
130
|
+
};
|
|
131
|
+
var Trigger = ({ children, ...rest }) => {
|
|
132
|
+
const { isOpen, id } = (0, import_react2.useContext)(CollapsibleContext);
|
|
133
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(React.Fragment, { children: React.cloneElement(children, {
|
|
134
|
+
"aria-controls": id,
|
|
135
|
+
"aria-expanded": !!isOpen,
|
|
136
|
+
...rest
|
|
137
|
+
}) });
|
|
138
|
+
};
|
|
139
|
+
Trigger.displayName = "Collapsible.Trigger";
|
|
140
|
+
var Panel = ({ children, ...rest }) => {
|
|
141
|
+
const {
|
|
142
|
+
isOpen,
|
|
143
|
+
id,
|
|
144
|
+
offset = 0,
|
|
145
|
+
collapsedHeight,
|
|
146
|
+
openHeight
|
|
147
|
+
} = (0, import_react2.useContext)(CollapsibleContext);
|
|
148
|
+
const ref = (0, import_react2.useRef)(null);
|
|
149
|
+
const measurement = q(ref);
|
|
150
|
+
const [isHidden, setIsHidden] = (0, import_react2.useState)(void 0);
|
|
151
|
+
const maxHeight = determineMaxHeight(
|
|
152
|
+
isHidden,
|
|
153
|
+
openHeight,
|
|
154
|
+
// Round up to the nearest pixel to prevent subpixel rendering issues
|
|
155
|
+
Math.ceil(measurement.height + offset)
|
|
156
|
+
);
|
|
157
|
+
(0, import_react2.useEffect)(() => {
|
|
158
|
+
if (!isOpen) {
|
|
159
|
+
const timeoutID = setTimeout(() => setIsHidden(!isOpen), 300);
|
|
160
|
+
return () => clearTimeout(timeoutID);
|
|
161
|
+
} else {
|
|
162
|
+
const timeoutID = setTimeout(() => setIsHidden(!isOpen), 0);
|
|
163
|
+
return () => clearTimeout(timeoutID);
|
|
164
|
+
}
|
|
165
|
+
}, [isOpen]);
|
|
166
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
167
|
+
CollapsingBox,
|
|
168
|
+
{
|
|
169
|
+
hasShadow: Boolean(collapsedHeight || openHeight && openHeight > 0),
|
|
170
|
+
scrollable: isOpen,
|
|
171
|
+
maxHeight: isOpen ? maxHeight : collapsedHeight,
|
|
172
|
+
minHeight: collapsedHeight,
|
|
173
|
+
"data-qa-collapsible": "",
|
|
174
|
+
"data-qa-collapsible-isopen": isOpen === true,
|
|
175
|
+
...rest,
|
|
176
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
177
|
+
import_seeds_react_box2.default,
|
|
178
|
+
{
|
|
179
|
+
width: "100%",
|
|
180
|
+
hidden: isHidden && collapsedHeight === 0,
|
|
181
|
+
"aria-hidden": !isOpen,
|
|
182
|
+
id,
|
|
183
|
+
ref,
|
|
184
|
+
children
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
Panel.displayName = "Collapsible.Panel";
|
|
191
|
+
Collapsible.Trigger = Trigger;
|
|
192
|
+
Collapsible.Panel = Panel;
|
|
193
|
+
var Collapsible_default = Collapsible;
|
|
194
|
+
|
|
195
|
+
// src/CollapsibleTypes.ts
|
|
196
|
+
var React2 = require("react");
|
|
197
|
+
|
|
198
|
+
// src/index.ts
|
|
199
|
+
var src_default = Collapsible_default;
|
|
200
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
201
|
+
0 && (module.exports = {
|
|
202
|
+
Collapsible
|
|
203
|
+
});
|
|
204
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Collapsible.tsx","../../seeds-react-hooks/src/useMeasure/useMeasure.ts","../../seeds-react-hooks/src/useSelect/useSelect.ts","../../seeds-react-hooks/src/useMultiselect/useMultiselect.ts","../../seeds-react-hooks/src/useMutationObserver/useMutationObserver.ts","../../seeds-react-hooks/src/useTextContent/useTextContent.ts","../../seeds-react-hooks/src/useWhyDidYouUpdate/useWhyDidYouUpdate.ts","../../seeds-react-hooks/src/useInteractiveColor/useInteractiveColor.ts","../src/styles.ts","../src/CollapsibleTypes.ts"],"sourcesContent":["import Collapsible from \"./Collapsible\";\n\nexport default Collapsible;\nexport { Collapsible };\nexport * from \"./CollapsibleTypes\";\n","import * as React from \"react\";\nimport { useState, useRef, useContext, useEffect } from \"react\";\nimport { useMeasure } from \"@sproutsocial/seeds-react-hooks\";\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport { CollapsingBox } from \"./styles\";\nimport type { TypeCollapsibleProps } from \"./CollapsibleTypes\";\n\nlet idCounter = 0;\n\ninterface TypeCollapsibleContext {\n isOpen?: boolean;\n id?: string;\n offset?: number;\n openHeight?: number;\n collapsedHeight?: number;\n}\n\nconst CollapsibleContext = React.createContext<TypeCollapsibleContext>({});\n\nconst Collapsible = ({\n children,\n isOpen = false,\n offset = 0,\n collapsedHeight = 0,\n openHeight,\n}: TypeCollapsibleProps) => {\n const [id] = useState(`Racine-collapsible-${idCounter++}`);\n return (\n <CollapsibleContext.Provider\n value={{\n isOpen,\n id,\n offset,\n collapsedHeight,\n openHeight,\n }}\n >\n {children}\n </CollapsibleContext.Provider>\n );\n};\n\nconst determineMaxHeight = (\n isHidden?: boolean,\n openHeight?: number,\n computedHeight?: number\n): number | undefined => {\n // If isHidden is undefined this is the first render. Return undefined so the max-height prop is not added\n // This is a hack to prevent css from animating if it begins in the open state\n // css animates when attribute values change (IE from 0 to another number)\n // css does not animate when simply adding an attribute to an HTML element\n if (isHidden === undefined) return undefined;\n // If the user has defined an explicit open height, return that as the max height\n if (openHeight) return openHeight;\n // Otherwise, fallback to the computed height\n return computedHeight;\n};\n\nconst Trigger = ({ children, ...rest }: { children: React.ReactElement }) => {\n const { isOpen, id } = useContext(CollapsibleContext);\n return (\n <React.Fragment>\n {React.cloneElement(children, {\n \"aria-controls\": id,\n \"aria-expanded\": !!isOpen,\n ...rest,\n })}\n </React.Fragment>\n );\n};\n\nTrigger.displayName = \"Collapsible.Trigger\";\n\nconst Panel = ({ children, ...rest }: { children: React.ReactNode }) => {\n const {\n isOpen,\n id,\n offset = 0,\n collapsedHeight,\n openHeight,\n } = useContext(CollapsibleContext);\n\n const ref = useRef<HTMLDivElement | null>(null);\n const measurement = useMeasure(ref);\n const [isHidden, setIsHidden] = useState<boolean | undefined>(undefined);\n const maxHeight = determineMaxHeight(\n isHidden,\n openHeight,\n // Round up to the nearest pixel to prevent subpixel rendering issues\n Math.ceil(measurement.height + offset)\n );\n\n /* We use the \"hidden\" attribute to remove the contents of the panel from the tab order of the page, but it interferes with the animation. This logic sets a slight timeout on setting the prop so that the animation has time to complete before the attribute is set. */\n useEffect(() => {\n if (!isOpen) {\n const timeoutID = setTimeout(() => setIsHidden(!isOpen), 300);\n return () => clearTimeout(timeoutID);\n } else {\n // Similar to the close animation, we need to delay setting hidden to run slightly async.\n // An issue occurs with the initial render isHidden logic that causes the animation to occur sporadically.\n // using this 0 second timeout just allows this component to initially render with an undefined max height,\n // Then go directly from undefined to the full max height, without a brief 0 value that triggers an animation\n const timeoutID = setTimeout(() => setIsHidden(!isOpen), 0);\n return () => clearTimeout(timeoutID);\n }\n }, [isOpen]);\n\n return (\n <CollapsingBox\n hasShadow={Boolean(collapsedHeight || (openHeight && openHeight > 0))}\n scrollable={isOpen}\n maxHeight={isOpen ? maxHeight : collapsedHeight}\n minHeight={collapsedHeight}\n data-qa-collapsible=\"\"\n data-qa-collapsible-isopen={isOpen === true}\n {...rest}\n >\n <Box\n width=\"100%\"\n hidden={isHidden && collapsedHeight === 0}\n aria-hidden={!isOpen}\n id={id}\n ref={ref}\n >\n {children}\n </Box>\n </CollapsingBox>\n );\n};\n\nPanel.displayName = \"Collapsible.Panel\";\n\nCollapsible.Trigger = Trigger;\nCollapsible.Panel = Panel;\n\nexport default Collapsible;\n","import { useState, useLayoutEffect, type RefObject } from \"react\";\n\ninterface DOMRectObject {\n x: number;\n y: number;\n width: number;\n height: number;\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\nconst initialBounds = Object.freeze({\n x: 0,\n y: 0,\n width: 0,\n height: 0,\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n});\n\nexport function useMeasure<TElement extends Element>(ref: RefObject<TElement>) {\n const [bounds, setContentRect] =\n useState<Readonly<DOMRectObject>>(initialBounds);\n\n useLayoutEffect(() => {\n const element = ref.current;\n\n if (\n !element ||\n // in non-browser environments (e.g. Jest tests) ResizeObserver is not defined\n !(\"ResizeObserver\" in window)\n ) {\n return;\n }\n\n const resizeObserver = new ResizeObserver(([entry]) => {\n if (!entry) return;\n const { x, y, width, height, top, right, bottom, left } =\n entry.contentRect;\n setContentRect({\n x,\n y,\n width,\n height,\n top,\n right,\n bottom,\n left,\n });\n });\n resizeObserver.observe(ref.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [ref]);\n\n return bounds;\n}\n","import { useState, useCallback } from \"react\";\n\ntype TypeSingleSelectProps<T extends string> = {\n initialValue?: T | \"\";\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: string | T) => any;\n};\n\nexport const useSelect = <T extends string>(\n {\n initialValue = \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeSingleSelectProps<T> = {\n initialValue: \"\",\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, setValue] = useState<string | T>(initialValue);\n\n const onChange = useCallback(\n (newValue: string) => {\n if (newValue !== value) {\n setValue(newValue);\n userOnChange(newValue);\n }\n },\n [userOnChange, value]\n );\n\n return { value, onChange };\n};\n","import { useCallback, useEffect, useReducer, useRef } from \"react\";\n\ntype TypeMultiSelectProps<T extends string> = {\n initialValue?: T[];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onChange?: (value: Array<string | T>) => any;\n};\n\nconst valueReducer = (\n state: Set<string>,\n action: { type: string; value?: string }\n): Set<string> => {\n const newState = new Set(state);\n switch (action.type) {\n case \"reset\": {\n return new Set();\n }\n case \"toggle_item\":\n default: {\n if (action.value) {\n if (newState.has(action.value)) {\n newState.delete(action.value);\n } else {\n newState.add(action.value);\n }\n }\n return newState;\n }\n }\n};\n\nexport const useMultiselect = <T extends string>(\n {\n initialValue = [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: userOnChange = () => {},\n }: TypeMultiSelectProps<T> = {\n initialValue: [],\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n onChange: () => {},\n }\n) => {\n const [value, dispatch] = useReducer(valueReducer, new Set(initialValue));\n\n const getArrayValue = (value: Set<string | T>) =>\n Array.from<string | T>(value);\n\n const onChange = useCallback(\n (newValue: string) => {\n dispatch({ type: \"toggle_item\", value: newValue });\n },\n [dispatch]\n );\n\n const isFirstRun = useRef(true);\n\n useEffect(() => {\n if (isFirstRun.current) {\n isFirstRun.current = false;\n return;\n }\n userOnChange(getArrayValue(value));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [userOnChange, value]);\n\n const onClear = useCallback(() => {\n dispatch({ type: \"reset\" });\n }, [dispatch]);\n\n return { value: getArrayValue(value), onChange, onClear };\n};\n","import { canUseDOM } from \"@sproutsocial/seeds-react-utilities\";\nimport { useEffect, useMemo, useState } from \"react\";\n\ntype TypeMutationObserverInitRequired =\n | {\n childList: true;\n }\n | {\n attributes: true;\n }\n | {\n characterData: true;\n };\n\ntype TypeMutationObserverInit = {\n subtree?: boolean;\n attributeOldValue?: boolean;\n characterDataOldValue?: boolean;\n attributeFilter?: Array<string>;\n} & TypeMutationObserverInitRequired;\n\ntype TypeMutationObserverCallback = (\n mutationList?: MutationRecord[],\n observer?: MutationObserver\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n) => any;\n\nconst defaultCallback: TypeMutationObserverCallback = (mutationList) =>\n mutationList;\n\nexport function useMutationObserver(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback = defaultCallback\n) {\n if (!canUseDOM()) {\n return;\n }\n /* eslint-disable-next-line */\n const [value, setValue] = useState(undefined);\n /* eslint-disable-next-line */\n const observer = useMemo(\n () =>\n new MutationObserver((mutationList, observer) => {\n const result = callback(mutationList, observer);\n setValue(result);\n }),\n [callback]\n );\n /* eslint-disable-next-line */\n useEffect(() => {\n if (targetNode) {\n observer.observe(targetNode, config);\n return () => {\n observer.disconnect();\n };\n }\n }, [targetNode, config, observer]);\n\n return value;\n}\n\nexport function useMutationObserverOnce(\n targetNode: Node | null,\n config: TypeMutationObserverInit,\n callback: TypeMutationObserverCallback\n) {\n const [isObserving, setObserving] = useState(true);\n const node = isObserving ? targetNode : null;\n const value = useMutationObserver(node, config, callback);\n if (value !== undefined && isObserving) {\n setObserving(false);\n }\n return value;\n}\n","import { useCallback, useState } from \"react\";\n\nexport type textContentRef = ((node: Node) => void) & { current?: string };\nexport function useTextContent(initial: string) {\n const [textContent, setTextContent] = useState(initial);\n\n const ref: textContentRef = useCallback((node: Node) => {\n if (node && node.textContent !== null) {\n setTextContent(node.textContent);\n }\n }, []);\n\n ref.current = textContent;\n return ref;\n}\n","import { useRef, useEffect } from \"react\";\n\nexport function useWhyDidYouUpdate(\n name: string,\n props: { [key: string]: any }\n) {\n // Get a mutable ref object where we can store props ...\n // ... for comparison next time this hook runs.\n const previousProps = useRef<typeof props>({});\n\n useEffect(() => {\n if (previousProps.current) {\n // Get all keys from previous and current props\n const allKeys = Object.keys({ ...previousProps.current, ...props });\n // Use this object to keep track of changed props\n const changesObj: typeof props = {};\n // Iterate through keys\n allKeys.forEach((key) => {\n // If previous is different from current\n\n if (previousProps.current[key] !== props[key]) {\n // Add to changesObj\n\n changesObj[key] = {\n from: previousProps.current[key],\n\n to: props[key],\n };\n }\n });\n\n // If changesObj not empty then output to console\n if (Object.keys(changesObj).length) {\n // eslint-disable-next-line no-console\n console.log(\"[why-did-you-update]\", name, changesObj);\n }\n }\n\n // Finally update previousProps with current props for next hook call\n previousProps.current = props;\n });\n}\n","import { darken, lighten } from \"polished\";\nimport { useTheme } from \"styled-components\";\nimport type { TypeTheme } from \"@sproutsocial/seeds-react-theme\";\n\n/**\n * The useInteractiveColor hook has context of theme mode (light or dark)\n * and can be used to lighten or darken a color dynamically\n *\n * note: colors are limited to our theme colors\n */\nconst useInteractiveColor = (themeColor: string): string => {\n // Throw error if used outside of a ThemeProvider (styled-components)\n if (!useTheme()) {\n throw new Error(\n \"useInteractiveColor() must be used within a Styled Components ThemeProvider\"\n );\n }\n\n // Get the current theme mode ie. 'light' or 'dark'\n const theme: TypeTheme = useTheme() as TypeTheme;\n const themeMode = theme.mode;\n\n // If the theme mode is dark, return a lightened version of the themeValue\n if (themeMode === \"dark\") {\n return lighten(0.2, themeColor);\n } else {\n // If the theme mode is light, return a darkened version of the themeValue\n return darken(0.2, themeColor);\n }\n};\n\nexport { useInteractiveColor };\n","import styled from \"styled-components\";\nimport Box from \"@sproutsocial/seeds-react-box\";\n\nexport const CollapsingBox = styled(Box)<{\n hasShadow?: boolean;\n scrollable?: boolean;\n}>`\n transition: max-height ${(p) => p.theme.duration.medium}\n ${(p) => p.theme.easing.ease_inout};\n will-change: max-height;\n position: relative;\n overflow: auto;\n ${({ hasShadow, scrollable }) =>\n hasShadow\n ? `background: /* Shadow covers */ linear-gradient(\n transparent 30%,\n rgba(255, 255, 255, 0)\n ),\n linear-gradient(rgba(255, 255, 255, 0), transparent 70%) 0 100%,\n /* Shadows */\n radial-gradient(\n farthest-side at 50% 0,\n rgb(39 51 51 / 5%),\n rgba(0, 0, 0, 0)\n ),\n radial-gradient(\n farthest-side at 50% 100%,\n rgb(39 51 51 / 5%),\n rgba(0, 0, 0, 0)\n )\n 0 100%;\n background-repeat: no-repeat;\n background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;\n background-attachment: local, local, scroll, scroll;\n ${scrollable ? `overflow: auto` : `overflow: hidden`};`\n : \"\"}\n`;\n","import * as React from \"react\";\n\n// The flow type is inexact but the underlying component does not accept any other props.\n// It might be worth extending the box props here for the refactor, but allowing it would provide no functionality right now.\nexport interface TypeCollapsibleProps {\n isOpen: boolean;\n children: React.ReactNode;\n\n /** If the children of the collapsible panel have a top or bottom margin, it will throw off the calculations for the height of the content. The total amount of vertical margin (in pixels) can be supplied to this prop to correct this. */\n offset?: number;\n collapsedHeight?: number;\n openHeight?: number;\n}\n"],"mappings":"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","names":["import_react","import_seeds_react_box","import_styled_components","styled","Box","Box","React"]}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sproutsocial/seeds-react-collapsible",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Seeds React Collapsible",
|
|
5
|
+
"author": "Sprout Social, Inc.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/esm/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup --dts",
|
|
12
|
+
"build:debug": "tsup --dts --metafile",
|
|
13
|
+
"dev": "tsup --watch --dts",
|
|
14
|
+
"clean": "rm -rf .turbo dist",
|
|
15
|
+
"clean:modules": "rm -rf node_modules",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:watch": "jest --watch --coverage=false"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@sproutsocial/seeds-react-box": "*"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.0.2",
|
|
25
|
+
"typescript": "^5.6.2",
|
|
26
|
+
"@types/react": "^18.0.0",
|
|
27
|
+
"@types/styled-components": "^5.1.26",
|
|
28
|
+
"react": "^18.0.0",
|
|
29
|
+
"styled-components": "^5.2.3",
|
|
30
|
+
"@sproutsocial/seeds-tsconfig": "*",
|
|
31
|
+
"@sproutsocial/seeds-testing": "*",
|
|
32
|
+
"@sproutsocial/seeds-react-testing-library": "*",
|
|
33
|
+
"@sproutsocial/seeds-react-button": "*"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"styled-components": "^5.2.3"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
|
+
import { Button } from "@sproutsocial/seeds-react-button";
|
|
4
|
+
import { Collapsible } from "./";
|
|
5
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
6
|
+
|
|
7
|
+
export interface TypeStatefulCollapseProps {
|
|
8
|
+
children: React.ReactElement;
|
|
9
|
+
offset?: number;
|
|
10
|
+
initialIsOpen?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const StatefulCollapse = ({
|
|
14
|
+
children,
|
|
15
|
+
offset = 0,
|
|
16
|
+
initialIsOpen = false,
|
|
17
|
+
}: TypeStatefulCollapseProps) => {
|
|
18
|
+
const [open, setOpen] = useState(initialIsOpen);
|
|
19
|
+
|
|
20
|
+
const toggle = () => setOpen(!open);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Collapsible isOpen={open} offset={offset}>
|
|
24
|
+
<Collapsible.Trigger>
|
|
25
|
+
<Button appearance="secondary" onClick={toggle}>
|
|
26
|
+
{open ? "Hide" : "Show"}
|
|
27
|
+
</Button>
|
|
28
|
+
</Collapsible.Trigger>
|
|
29
|
+
|
|
30
|
+
<Collapsible.Panel>{children}</Collapsible.Panel>
|
|
31
|
+
</Collapsible>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const meta: Meta<typeof Collapsible> = {
|
|
36
|
+
title: "Components/Collapsible",
|
|
37
|
+
component: Collapsible,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
|
|
42
|
+
type Story = StoryObj<typeof Collapsible>;
|
|
43
|
+
|
|
44
|
+
export const DefaultStory: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<StatefulCollapse>
|
|
47
|
+
<Box width="100%" height="200px" bg="container.background.base" p={400}>
|
|
48
|
+
<Button appearance="secondary">A button</Button>
|
|
49
|
+
</Box>
|
|
50
|
+
</StatefulCollapse>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const InitialIsOpen: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<StatefulCollapse initialIsOpen>
|
|
57
|
+
<Box width="100%" height="200px" bg="container.background.base" p={400}>
|
|
58
|
+
<Button appearance="secondary">A button</Button>
|
|
59
|
+
</Box>
|
|
60
|
+
</StatefulCollapse>
|
|
61
|
+
),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const WithOffset: Story = {
|
|
65
|
+
render: () => (
|
|
66
|
+
<StatefulCollapse offset={100}>
|
|
67
|
+
<Box
|
|
68
|
+
width="100%"
|
|
69
|
+
height="200px"
|
|
70
|
+
bg="container.background.base"
|
|
71
|
+
p={400}
|
|
72
|
+
mt="100px"
|
|
73
|
+
>
|
|
74
|
+
<Button appearance="secondary">A button</Button>
|
|
75
|
+
</Box>
|
|
76
|
+
</StatefulCollapse>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const WithShortContent: Story = {
|
|
81
|
+
render: () => (
|
|
82
|
+
<StatefulCollapse>
|
|
83
|
+
<Box width="15%" height="50px" bg="container.background.base" p={400}>
|
|
84
|
+
Hello.
|
|
85
|
+
</Box>
|
|
86
|
+
</StatefulCollapse>
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const WithTallContent: Story = {
|
|
91
|
+
render: () => (
|
|
92
|
+
<StatefulCollapse>
|
|
93
|
+
<Box width="15%" height="200vh" bg="container.background.base" p={400}>
|
|
94
|
+
Hello.
|
|
95
|
+
</Box>
|
|
96
|
+
</StatefulCollapse>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
interface TypeStatefulCollapseMinHeightProps {
|
|
101
|
+
children: React.ReactElement;
|
|
102
|
+
offset?: number;
|
|
103
|
+
collapsedHeight?: number;
|
|
104
|
+
openHeight?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const StatefulCollapseWithMinHeight = ({
|
|
108
|
+
children,
|
|
109
|
+
offset = 0,
|
|
110
|
+
collapsedHeight = 0,
|
|
111
|
+
openHeight,
|
|
112
|
+
}: TypeStatefulCollapseMinHeightProps) => {
|
|
113
|
+
const [open, setOpen] = useState(false);
|
|
114
|
+
|
|
115
|
+
const toggle = () => setOpen(!open);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Collapsible
|
|
119
|
+
isOpen={open}
|
|
120
|
+
offset={offset}
|
|
121
|
+
openHeight={openHeight}
|
|
122
|
+
collapsedHeight={collapsedHeight}
|
|
123
|
+
>
|
|
124
|
+
<Collapsible.Panel>{children}</Collapsible.Panel>
|
|
125
|
+
<Collapsible.Trigger>
|
|
126
|
+
<Button onClick={toggle}>{open ? "Show Less" : "Show More"}</Button>
|
|
127
|
+
</Collapsible.Trigger>
|
|
128
|
+
</Collapsible>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const WithCollapsedHeight: Story = {
|
|
133
|
+
render: () => (
|
|
134
|
+
<StatefulCollapseWithMinHeight collapsedHeight={100} openHeight={300}>
|
|
135
|
+
<Box width="500px" p={400}>
|
|
136
|
+
Threepio! Come in, Threepio! Threepio! Get to the top! I can’t Where
|
|
137
|
+
could he be? Threepio! Threepio, will you come in? They aren’t here!
|
|
138
|
+
Something must have happened to them. See if they’ve been captured.
|
|
139
|
+
Hurry! One thing’s for sure. We’re all going to be a lot thinner! Get on
|
|
140
|
+
top of it! I’m trying! Thank goodness, they haven’t found them! Where
|
|
141
|
+
could they be? Use the comlink? Oh, my! I forgot I turned it off! Are
|
|
142
|
+
you there, sir? Threepio! We’ve had some problems… Will you shut up and
|
|
143
|
+
listen to me? Shut down all garbage mashers on the detention level, will
|
|
144
|
+
you? Do you copy? Shut down all the garbage mashers on the detention
|
|
145
|
+
level. Shut down all the garbage mashers on the detention level. No.
|
|
146
|
+
Shut them all down! Hurry! Listen to them! They’re dying, Artoo! Curse
|
|
147
|
+
my metal body! I wasn’t fast enough. It’s all my fault! My poor master!
|
|
148
|
+
Threepio, we’re all right! We’re all right. You did great. Threepio!
|
|
149
|
+
Come in, Threepio! Threepio! Get to the top! I can’t Where could he be?
|
|
150
|
+
Threepio! Threepio, will you come in? They aren’t here! Something must
|
|
151
|
+
have happened to them. See if they’ve been captured. Hurry! One thing’s
|
|
152
|
+
for sure. We’re all going to be a lot thinner! Get on top of it! I’m
|
|
153
|
+
trying! Thank goodness, they haven’t found them! Where could they be?
|
|
154
|
+
Use the comlink? Oh, my! I forgot I turned it off! Are you there, sir?
|
|
155
|
+
Threepio! We’ve had some problems… Will you shut up and listen to me?
|
|
156
|
+
Shut down all garbage mashers on the detention level, will you? Do you
|
|
157
|
+
copy? Shut down all the garbage mashers on the detention level. Shut
|
|
158
|
+
down all the garbage mashers on the detention level. No. Shut them all
|
|
159
|
+
down! Hurry! Listen to them! They’re dying, Artoo! Curse my metal body!
|
|
160
|
+
I wasn’t fast enough. It’s all my fault! My poor master! Threepio, we’re
|
|
161
|
+
all right! We’re all right. You did great.
|
|
162
|
+
</Box>
|
|
163
|
+
</StatefulCollapseWithMinHeight>
|
|
164
|
+
),
|
|
165
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useState, useRef, useContext, useEffect } from "react";
|
|
3
|
+
import { useMeasure } from "@sproutsocial/seeds-react-hooks";
|
|
4
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
5
|
+
import { CollapsingBox } from "./styles";
|
|
6
|
+
import type { TypeCollapsibleProps } from "./CollapsibleTypes";
|
|
7
|
+
|
|
8
|
+
let idCounter = 0;
|
|
9
|
+
|
|
10
|
+
interface TypeCollapsibleContext {
|
|
11
|
+
isOpen?: boolean;
|
|
12
|
+
id?: string;
|
|
13
|
+
offset?: number;
|
|
14
|
+
openHeight?: number;
|
|
15
|
+
collapsedHeight?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CollapsibleContext = React.createContext<TypeCollapsibleContext>({});
|
|
19
|
+
|
|
20
|
+
const Collapsible = ({
|
|
21
|
+
children,
|
|
22
|
+
isOpen = false,
|
|
23
|
+
offset = 0,
|
|
24
|
+
collapsedHeight = 0,
|
|
25
|
+
openHeight,
|
|
26
|
+
}: TypeCollapsibleProps) => {
|
|
27
|
+
const [id] = useState(`Racine-collapsible-${idCounter++}`);
|
|
28
|
+
return (
|
|
29
|
+
<CollapsibleContext.Provider
|
|
30
|
+
value={{
|
|
31
|
+
isOpen,
|
|
32
|
+
id,
|
|
33
|
+
offset,
|
|
34
|
+
collapsedHeight,
|
|
35
|
+
openHeight,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</CollapsibleContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const determineMaxHeight = (
|
|
44
|
+
isHidden?: boolean,
|
|
45
|
+
openHeight?: number,
|
|
46
|
+
computedHeight?: number
|
|
47
|
+
): number | undefined => {
|
|
48
|
+
// If isHidden is undefined this is the first render. Return undefined so the max-height prop is not added
|
|
49
|
+
// This is a hack to prevent css from animating if it begins in the open state
|
|
50
|
+
// css animates when attribute values change (IE from 0 to another number)
|
|
51
|
+
// css does not animate when simply adding an attribute to an HTML element
|
|
52
|
+
if (isHidden === undefined) return undefined;
|
|
53
|
+
// If the user has defined an explicit open height, return that as the max height
|
|
54
|
+
if (openHeight) return openHeight;
|
|
55
|
+
// Otherwise, fallback to the computed height
|
|
56
|
+
return computedHeight;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const Trigger = ({ children, ...rest }: { children: React.ReactElement }) => {
|
|
60
|
+
const { isOpen, id } = useContext(CollapsibleContext);
|
|
61
|
+
return (
|
|
62
|
+
<React.Fragment>
|
|
63
|
+
{React.cloneElement(children, {
|
|
64
|
+
"aria-controls": id,
|
|
65
|
+
"aria-expanded": !!isOpen,
|
|
66
|
+
...rest,
|
|
67
|
+
})}
|
|
68
|
+
</React.Fragment>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
Trigger.displayName = "Collapsible.Trigger";
|
|
73
|
+
|
|
74
|
+
const Panel = ({ children, ...rest }: { children: React.ReactNode }) => {
|
|
75
|
+
const {
|
|
76
|
+
isOpen,
|
|
77
|
+
id,
|
|
78
|
+
offset = 0,
|
|
79
|
+
collapsedHeight,
|
|
80
|
+
openHeight,
|
|
81
|
+
} = useContext(CollapsibleContext);
|
|
82
|
+
|
|
83
|
+
const ref = useRef<HTMLDivElement | null>(null);
|
|
84
|
+
const measurement = useMeasure(ref);
|
|
85
|
+
const [isHidden, setIsHidden] = useState<boolean | undefined>(undefined);
|
|
86
|
+
const maxHeight = determineMaxHeight(
|
|
87
|
+
isHidden,
|
|
88
|
+
openHeight,
|
|
89
|
+
// Round up to the nearest pixel to prevent subpixel rendering issues
|
|
90
|
+
Math.ceil(measurement.height + offset)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
/* We use the "hidden" attribute to remove the contents of the panel from the tab order of the page, but it interferes with the animation. This logic sets a slight timeout on setting the prop so that the animation has time to complete before the attribute is set. */
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!isOpen) {
|
|
96
|
+
const timeoutID = setTimeout(() => setIsHidden(!isOpen), 300);
|
|
97
|
+
return () => clearTimeout(timeoutID);
|
|
98
|
+
} else {
|
|
99
|
+
// Similar to the close animation, we need to delay setting hidden to run slightly async.
|
|
100
|
+
// An issue occurs with the initial render isHidden logic that causes the animation to occur sporadically.
|
|
101
|
+
// using this 0 second timeout just allows this component to initially render with an undefined max height,
|
|
102
|
+
// Then go directly from undefined to the full max height, without a brief 0 value that triggers an animation
|
|
103
|
+
const timeoutID = setTimeout(() => setIsHidden(!isOpen), 0);
|
|
104
|
+
return () => clearTimeout(timeoutID);
|
|
105
|
+
}
|
|
106
|
+
}, [isOpen]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<CollapsingBox
|
|
110
|
+
hasShadow={Boolean(collapsedHeight || (openHeight && openHeight > 0))}
|
|
111
|
+
scrollable={isOpen}
|
|
112
|
+
maxHeight={isOpen ? maxHeight : collapsedHeight}
|
|
113
|
+
minHeight={collapsedHeight}
|
|
114
|
+
data-qa-collapsible=""
|
|
115
|
+
data-qa-collapsible-isopen={isOpen === true}
|
|
116
|
+
{...rest}
|
|
117
|
+
>
|
|
118
|
+
<Box
|
|
119
|
+
width="100%"
|
|
120
|
+
hidden={isHidden && collapsedHeight === 0}
|
|
121
|
+
aria-hidden={!isOpen}
|
|
122
|
+
id={id}
|
|
123
|
+
ref={ref}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</Box>
|
|
127
|
+
</CollapsingBox>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
Panel.displayName = "Collapsible.Panel";
|
|
132
|
+
|
|
133
|
+
Collapsible.Trigger = Trigger;
|
|
134
|
+
Collapsible.Panel = Panel;
|
|
135
|
+
|
|
136
|
+
export default Collapsible;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
// The flow type is inexact but the underlying component does not accept any other props.
|
|
4
|
+
// It might be worth extending the box props here for the refactor, but allowing it would provide no functionality right now.
|
|
5
|
+
export interface TypeCollapsibleProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
|
|
9
|
+
/** If the children of the collapsible panel have a top or bottom margin, it will throw off the calculations for the height of the content. The total amount of vertical margin (in pixels) can be supplied to this prop to correct this. */
|
|
10
|
+
offset?: number;
|
|
11
|
+
collapsedHeight?: number;
|
|
12
|
+
openHeight?: number;
|
|
13
|
+
}
|
package/src/index.ts
ADDED
package/src/styles.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
import Box from "@sproutsocial/seeds-react-box";
|
|
3
|
+
|
|
4
|
+
export const CollapsingBox = styled(Box)<{
|
|
5
|
+
hasShadow?: boolean;
|
|
6
|
+
scrollable?: boolean;
|
|
7
|
+
}>`
|
|
8
|
+
transition: max-height ${(p) => p.theme.duration.medium}
|
|
9
|
+
${(p) => p.theme.easing.ease_inout};
|
|
10
|
+
will-change: max-height;
|
|
11
|
+
position: relative;
|
|
12
|
+
overflow: auto;
|
|
13
|
+
${({ hasShadow, scrollable }) =>
|
|
14
|
+
hasShadow
|
|
15
|
+
? `background: /* Shadow covers */ linear-gradient(
|
|
16
|
+
transparent 30%,
|
|
17
|
+
rgba(255, 255, 255, 0)
|
|
18
|
+
),
|
|
19
|
+
linear-gradient(rgba(255, 255, 255, 0), transparent 70%) 0 100%,
|
|
20
|
+
/* Shadows */
|
|
21
|
+
radial-gradient(
|
|
22
|
+
farthest-side at 50% 0,
|
|
23
|
+
rgb(39 51 51 / 5%),
|
|
24
|
+
rgba(0, 0, 0, 0)
|
|
25
|
+
),
|
|
26
|
+
radial-gradient(
|
|
27
|
+
farthest-side at 50% 100%,
|
|
28
|
+
rgb(39 51 51 / 5%),
|
|
29
|
+
rgba(0, 0, 0, 0)
|
|
30
|
+
)
|
|
31
|
+
0 100%;
|
|
32
|
+
background-repeat: no-repeat;
|
|
33
|
+
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
|
|
34
|
+
background-attachment: local, local, scroll, scroll;
|
|
35
|
+
${scrollable ? `overflow: auto` : `overflow: hidden`};`
|
|
36
|
+
: ""}
|
|
37
|
+
`;
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig((options) => ({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["cjs", "esm"],
|
|
6
|
+
clean: true,
|
|
7
|
+
legacyOutput: true,
|
|
8
|
+
dts: options.dts,
|
|
9
|
+
external: ["react"],
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
metafile: options.metafile,
|
|
12
|
+
}));
|