@sproutsocial/seeds-react-content-block 0.2.11 → 0.3.2
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +53 -0
- package/dist/esm/index.js +72 -38
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.mts +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.js +68 -47
- package/dist/index.js.map +1 -1
- package/package.json +8 -9
- package/src/ContentBlock.stories.tsx +68 -0
- package/src/ContentBlock.tsx +116 -33
- package/src/__tests__/content-block.test.tsx +153 -1
- package/src/styles.ts +0 -10
package/.turbo/turbo-build.log
CHANGED
|
@@ -3,19 +3,19 @@ $ tsup --dts
|
|
|
3
3
|
[34mCLI[39m Building entry: src/index.ts
|
|
4
4
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
5
5
|
[34mCLI[39m tsup v8.5.0
|
|
6
|
-
[34mCLI[39m Using tsup config: /home/runner/
|
|
6
|
+
[34mCLI[39m Using tsup config: /home/runner/_work/seeds/seeds/seeds-react/seeds-react-content-block/tsup.config.ts
|
|
7
7
|
[34mCLI[39m Target: es2022
|
|
8
8
|
[34mCLI[39m Cleaning output folder
|
|
9
9
|
[34mCJS[39m Build start
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mCJS[39m [1mdist/index.js [22m[32m4.
|
|
12
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
13
|
-
[32mCJS[39m ⚡️ Build success in
|
|
14
|
-
[32mESM[39m [1mdist/esm/index.js [22m[32m2.
|
|
15
|
-
[32mESM[39m [1mdist/esm/index.js.map [22m[
|
|
16
|
-
[32mESM[39m ⚡️ Build success in
|
|
11
|
+
[32mCJS[39m [1mdist/index.js [22m[32m4.32 KB[39m
|
|
12
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m5.44 KB[39m
|
|
13
|
+
[32mCJS[39m ⚡️ Build success in 13ms
|
|
14
|
+
[32mESM[39m [1mdist/esm/index.js [22m[32m2.67 KB[39m
|
|
15
|
+
[32mESM[39m [1mdist/esm/index.js.map [22m[32m5.44 KB[39m
|
|
16
|
+
[32mESM[39m ⚡️ Build success in 13ms
|
|
17
17
|
[34mDTS[39m Build start
|
|
18
|
-
[32mDTS[39m ⚡️ Build success in
|
|
19
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
21
|
-
Done in
|
|
18
|
+
[32mDTS[39m ⚡️ Build success in 1433ms
|
|
19
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.29 KB[39m
|
|
20
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.29 KB[39m
|
|
21
|
+
Done in 2.15s.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# @sproutsocial/seeds-react-content-block
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [7d54d67]
|
|
8
|
+
- @sproutsocial/seeds-react-button@1.4.0
|
|
9
|
+
|
|
10
|
+
## 0.3.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [17d4f12]
|
|
15
|
+
- @sproutsocial/seeds-react-theme@3.6.0
|
|
16
|
+
- @sproutsocial/seeds-react-box@1.1.14
|
|
17
|
+
- @sproutsocial/seeds-react-container@0.3.8
|
|
18
|
+
- @sproutsocial/seeds-react-loader@1.0.14
|
|
19
|
+
- @sproutsocial/seeds-react-content-header@0.2.1
|
|
20
|
+
- @sproutsocial/seeds-react-button@1.3.20
|
|
21
|
+
|
|
22
|
+
## 0.3.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- 5bb63e1: ContentBlock now uses ContentHeader/CollapsibleContentHeader internally and supports optional collapsible behavior via isCollapsible prop.
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- Updated dependencies [5bb63e1]
|
|
31
|
+
- Updated dependencies [5bb63e1]
|
|
32
|
+
- @sproutsocial/seeds-react-content-header@0.2.0
|
|
33
|
+
- @sproutsocial/seeds-react-container@0.3.7
|
|
34
|
+
- @sproutsocial/seeds-react-button@1.3.19
|
|
35
|
+
- @sproutsocial/seeds-react-loader@1.0.13
|
|
36
|
+
|
|
37
|
+
## 0.2.13
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- Updated dependencies [118e300]
|
|
42
|
+
- @sproutsocial/seeds-react-theme@3.5.1
|
|
43
|
+
- @sproutsocial/seeds-react-box@1.1.13
|
|
44
|
+
- @sproutsocial/seeds-react-container@0.3.6
|
|
45
|
+
- @sproutsocial/seeds-react-icon@2.2.4
|
|
46
|
+
- @sproutsocial/seeds-react-loader@1.0.12
|
|
47
|
+
- @sproutsocial/seeds-react-button@1.3.18
|
|
48
|
+
|
|
49
|
+
## 0.2.12
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- @sproutsocial/seeds-react-icon@2.2.3
|
|
54
|
+
- @sproutsocial/seeds-react-button@1.3.17
|
|
55
|
+
|
|
3
56
|
## 0.2.11
|
|
4
57
|
|
|
5
58
|
### Patch Changes
|
package/dist/esm/index.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
// src/ContentBlock.tsx
|
|
2
|
-
import "react";
|
|
3
|
-
import { Box as Box2 } from "@sproutsocial/seeds-react-box";
|
|
4
|
-
import { Text } from "@sproutsocial/seeds-react-text";
|
|
5
|
-
import { Loader } from "@sproutsocial/seeds-react-loader";
|
|
6
|
-
|
|
7
|
-
// src/styles.ts
|
|
8
|
-
import styled from "styled-components";
|
|
2
|
+
import { useState } from "react";
|
|
9
3
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
color: ${({ theme }) => theme.colors.text.headline};
|
|
17
|
-
padding: ${({ theme }) => theme.space[400]};
|
|
18
|
-
`;
|
|
19
|
-
var ContentBlockContent = styled(Box)`
|
|
20
|
-
padding: ${({ theme }) => theme.space[400]};
|
|
21
|
-
`;
|
|
22
|
-
|
|
23
|
-
// src/ContentBlock.tsx
|
|
4
|
+
import { Loader } from "@sproutsocial/seeds-react-loader";
|
|
5
|
+
import { Collapsible } from "@sproutsocial/seeds-react-collapsible";
|
|
6
|
+
import {
|
|
7
|
+
ContentHeader,
|
|
8
|
+
CollapsibleContentHeader
|
|
9
|
+
} from "@sproutsocial/seeds-react-content-header";
|
|
24
10
|
import { Container } from "@sproutsocial/seeds-react-container";
|
|
25
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
26
|
-
var
|
|
12
|
+
var ContentArea = ({
|
|
13
|
+
isLoading,
|
|
14
|
+
children,
|
|
15
|
+
contentProps
|
|
16
|
+
}) => /* @__PURE__ */ jsx(
|
|
17
|
+
Box,
|
|
18
|
+
{
|
|
19
|
+
p: 400,
|
|
20
|
+
...contentProps,
|
|
21
|
+
borderBottomLeftRadius: "outer",
|
|
22
|
+
borderBottomRightRadius: "outer",
|
|
23
|
+
children: isLoading ? /* @__PURE__ */ jsx(Box, { my: 400, children: /* @__PURE__ */ jsx(Loader, { delay: false }) }) : children
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
var CollapsibleContentBlock = ({
|
|
27
27
|
title,
|
|
28
28
|
subtitle,
|
|
29
29
|
titleAs = "h2",
|
|
@@ -32,26 +32,60 @@ var ContentBlock = ({
|
|
|
32
32
|
isLoading,
|
|
33
33
|
headerActions,
|
|
34
34
|
contentProps,
|
|
35
|
-
|
|
35
|
+
isOpen: controlledIsOpen,
|
|
36
|
+
defaultOpen = true,
|
|
37
|
+
onOpenChange
|
|
36
38
|
}) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
40
|
+
const open = controlledIsOpen !== void 0 ? controlledIsOpen : internalOpen;
|
|
41
|
+
const handleOpenChange = (newOpen) => {
|
|
42
|
+
if (controlledIsOpen === void 0) {
|
|
43
|
+
setInternalOpen(newOpen);
|
|
44
|
+
}
|
|
45
|
+
onOpenChange?.(newOpen);
|
|
46
|
+
};
|
|
47
|
+
return /* @__PURE__ */ jsx(Collapsible, { isOpen: open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxs(Container, { as: "section", children: [
|
|
48
|
+
/* @__PURE__ */ jsx(
|
|
49
|
+
CollapsibleContentHeader,
|
|
50
|
+
{
|
|
51
|
+
title,
|
|
52
|
+
subtitle,
|
|
53
|
+
headingLevel: titleAs,
|
|
54
|
+
subtitleAs,
|
|
55
|
+
headerActions,
|
|
56
|
+
trigger: Collapsible.Trigger,
|
|
57
|
+
triggerPosition: "left"
|
|
58
|
+
}
|
|
59
|
+
),
|
|
60
|
+
/* @__PURE__ */ jsx(Collapsible.Panel, { children: /* @__PURE__ */ jsx(ContentArea, { isLoading, contentProps, children }) })
|
|
61
|
+
] }) });
|
|
62
|
+
};
|
|
63
|
+
var ContentBlock = ({ isCollapsible, ...props }) => {
|
|
64
|
+
if (isCollapsible) {
|
|
65
|
+
return /* @__PURE__ */ jsx(CollapsibleContentBlock, { ...props });
|
|
66
|
+
}
|
|
67
|
+
const {
|
|
68
|
+
title,
|
|
69
|
+
subtitle,
|
|
70
|
+
titleAs = "h2",
|
|
71
|
+
subtitleAs = "h3",
|
|
72
|
+
children,
|
|
73
|
+
isLoading,
|
|
74
|
+
headerActions,
|
|
75
|
+
contentProps
|
|
76
|
+
} = props;
|
|
77
|
+
return /* @__PURE__ */ jsxs(Container, { as: "section", children: [
|
|
45
78
|
/* @__PURE__ */ jsx(
|
|
46
|
-
|
|
79
|
+
ContentHeader,
|
|
47
80
|
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
title,
|
|
82
|
+
subtitle,
|
|
83
|
+
headingLevel: titleAs,
|
|
84
|
+
subtitleAs,
|
|
85
|
+
headerActions
|
|
53
86
|
}
|
|
54
|
-
)
|
|
87
|
+
),
|
|
88
|
+
/* @__PURE__ */ jsx(ContentArea, { isLoading, contentProps, children })
|
|
55
89
|
] });
|
|
56
90
|
};
|
|
57
91
|
var ContentBlock_default = ContentBlock;
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/ContentBlock.tsx","../../src/
|
|
1
|
+
{"version":3,"sources":["../../src/ContentBlock.tsx","../../src/index.ts"],"sourcesContent":["import React, { useState } from \"react\";\nimport { Box } from \"@sproutsocial/seeds-react-box\";\nimport { Loader } from \"@sproutsocial/seeds-react-loader\";\nimport { Collapsible } from \"@sproutsocial/seeds-react-collapsible\";\nimport {\n ContentHeader,\n CollapsibleContentHeader,\n} from \"@sproutsocial/seeds-react-content-header\";\nimport { Container } from \"@sproutsocial/seeds-react-container\";\n\ninterface ContentBlockBaseProps {\n title: string;\n subtitle: string;\n titleAs?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n subtitleAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"p\";\n isLoading?: boolean;\n children: React.ReactNode;\n headerActions?: React.ReactNode;\n contentProps?: React.ComponentProps<typeof Box>;\n}\n\ntype StaticContentBlockProps = ContentBlockBaseProps & {\n isCollapsible?: false;\n isOpen?: never;\n defaultOpen?: never;\n onOpenChange?: never;\n};\n\ntype CollapsibleContentBlockProps = ContentBlockBaseProps & {\n /** Enables collapse/expand behavior on the content area */\n isCollapsible: true;\n /** Controlled open state. When provided, the component is in controlled mode. */\n isOpen?: boolean;\n /** Initial open state for uncontrolled mode. Defaults to true. */\n defaultOpen?: boolean;\n /** Called when the open state changes */\n onOpenChange?: (open: boolean) => void;\n};\n\nexport type ContentBlockProps =\n | StaticContentBlockProps\n | CollapsibleContentBlockProps;\n\nconst ContentArea = ({\n isLoading,\n children,\n contentProps,\n}: Pick<ContentBlockBaseProps, \"isLoading\" | \"children\" | \"contentProps\">) => (\n <Box\n p={400}\n {...contentProps}\n borderBottomLeftRadius=\"outer\"\n borderBottomRightRadius=\"outer\"\n >\n {isLoading ? (\n <Box my={400}>\n <Loader delay={false} />\n </Box>\n ) : (\n children\n )}\n </Box>\n);\n\nconst CollapsibleContentBlock = ({\n title,\n subtitle,\n titleAs = \"h2\",\n subtitleAs = \"h3\",\n children,\n isLoading,\n headerActions,\n contentProps,\n isOpen: controlledIsOpen,\n defaultOpen = true,\n onOpenChange,\n}: Omit<CollapsibleContentBlockProps, \"isCollapsible\">) => {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = controlledIsOpen !== undefined ? controlledIsOpen : internalOpen;\n\n const handleOpenChange = (newOpen: boolean) => {\n if (controlledIsOpen === undefined) {\n setInternalOpen(newOpen);\n }\n onOpenChange?.(newOpen);\n };\n\n return (\n <Collapsible isOpen={open} onOpenChange={handleOpenChange}>\n <Container as=\"section\">\n <CollapsibleContentHeader\n title={title}\n subtitle={subtitle}\n headingLevel={titleAs}\n subtitleAs={subtitleAs}\n headerActions={headerActions}\n trigger={Collapsible.Trigger}\n triggerPosition=\"left\"\n />\n <Collapsible.Panel>\n <ContentArea isLoading={isLoading} contentProps={contentProps}>\n {children}\n </ContentArea>\n </Collapsible.Panel>\n </Container>\n </Collapsible>\n );\n};\n\nconst ContentBlock = ({ isCollapsible, ...props }: ContentBlockProps) => {\n if (isCollapsible) {\n return <CollapsibleContentBlock {...props} />;\n }\n\n const {\n title,\n subtitle,\n titleAs = \"h2\",\n subtitleAs = \"h3\",\n children,\n isLoading,\n headerActions,\n contentProps,\n } = props;\n\n return (\n <Container as=\"section\">\n <ContentHeader\n title={title}\n subtitle={subtitle}\n headingLevel={titleAs}\n subtitleAs={subtitleAs}\n headerActions={headerActions}\n />\n <ContentArea isLoading={isLoading} contentProps={contentProps}>\n {children}\n </ContentArea>\n </Container>\n );\n};\n\nexport default ContentBlock;\n","import ContentBlock from \"./ContentBlock\";\n\nexport type { ContentBlockProps } from \"./ContentBlock\";\n\nexport default ContentBlock;\nexport { ContentBlock };\n"],"mappings":";AAAA,SAAgB,gBAAgB;AAChC,SAAS,WAAW;AACpB,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB;AAgDlB,cAiCF,YAjCE;AAbR,IAAM,cAAc,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,MACE;AAAA,EAAC;AAAA;AAAA,IACC,GAAG;AAAA,IACF,GAAG;AAAA,IACJ,wBAAuB;AAAA,IACvB,yBAAwB;AAAA,IAEvB,sBACC,oBAAC,OAAI,IAAI,KACP,8BAAC,UAAO,OAAO,OAAO,GACxB,IAEA;AAAA;AAEJ;AAGF,IAAM,0BAA0B,CAAC;AAAA,EAC/B;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AACF,MAA2D;AACzD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,WAAW;AAC5D,QAAM,OAAO,qBAAqB,SAAY,mBAAmB;AAEjE,QAAM,mBAAmB,CAAC,YAAqB;AAC7C,QAAI,qBAAqB,QAAW;AAClC,sBAAgB,OAAO;AAAA,IACzB;AACA,mBAAe,OAAO;AAAA,EACxB;AAEA,SACE,oBAAC,eAAY,QAAQ,MAAM,cAAc,kBACvC,+BAAC,aAAU,IAAG,WACZ;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,cAAc;AAAA,QACd;AAAA,QACA;AAAA,QACA,SAAS,YAAY;AAAA,QACrB,iBAAgB;AAAA;AAAA,IAClB;AAAA,IACA,oBAAC,YAAY,OAAZ,EACC,8BAAC,eAAY,WAAsB,cAChC,UACH,GACF;AAAA,KACF,GACF;AAEJ;AAEA,IAAM,eAAe,CAAC,EAAE,eAAe,GAAG,MAAM,MAAyB;AACvE,MAAI,eAAe;AACjB,WAAO,oBAAC,2BAAyB,GAAG,OAAO;AAAA,EAC7C;AAEA,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,SACE,qBAAC,aAAU,IAAG,WACZ;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,cAAc;AAAA,QACd;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACA,oBAAC,eAAY,WAAsB,cAChC,UACH;AAAA,KACF;AAEJ;AAEA,IAAO,uBAAQ;;;ACzIf,IAAO,gBAAQ;","names":[]}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { Box } from '@sproutsocial/seeds-react-box';
|
|
3
4
|
|
|
4
|
-
interface
|
|
5
|
+
interface ContentBlockBaseProps {
|
|
5
6
|
title: string;
|
|
6
7
|
subtitle: string;
|
|
7
|
-
titleAs
|
|
8
|
-
subtitleAs
|
|
8
|
+
titleAs?: "h1" | "h2" | "h3" | "h4";
|
|
9
|
+
subtitleAs?: "h2" | "h3" | "h4" | "h5" | "p";
|
|
9
10
|
isLoading?: boolean;
|
|
10
11
|
children: React.ReactNode;
|
|
11
12
|
headerActions?: React.ReactNode;
|
|
12
13
|
contentProps?: React.ComponentProps<typeof Box>;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
type StaticContentBlockProps = ContentBlockBaseProps & {
|
|
16
|
+
isCollapsible?: false;
|
|
17
|
+
isOpen?: never;
|
|
18
|
+
defaultOpen?: never;
|
|
19
|
+
onOpenChange?: never;
|
|
20
|
+
};
|
|
21
|
+
type CollapsibleContentBlockProps = ContentBlockBaseProps & {
|
|
22
|
+
/** Enables collapse/expand behavior on the content area */
|
|
23
|
+
isCollapsible: true;
|
|
24
|
+
/** Controlled open state. When provided, the component is in controlled mode. */
|
|
25
|
+
isOpen?: boolean;
|
|
26
|
+
/** Initial open state for uncontrolled mode. Defaults to true. */
|
|
27
|
+
defaultOpen?: boolean;
|
|
28
|
+
/** Called when the open state changes */
|
|
29
|
+
onOpenChange?: (open: boolean) => void;
|
|
30
|
+
};
|
|
31
|
+
type ContentBlockProps = StaticContentBlockProps | CollapsibleContentBlockProps;
|
|
32
|
+
declare const ContentBlock: ({ isCollapsible, ...props }: ContentBlockProps) => react_jsx_runtime.JSX.Element;
|
|
15
33
|
|
|
16
34
|
export { ContentBlock, type ContentBlockProps, ContentBlock as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { Box } from '@sproutsocial/seeds-react-box';
|
|
3
4
|
|
|
4
|
-
interface
|
|
5
|
+
interface ContentBlockBaseProps {
|
|
5
6
|
title: string;
|
|
6
7
|
subtitle: string;
|
|
7
|
-
titleAs
|
|
8
|
-
subtitleAs
|
|
8
|
+
titleAs?: "h1" | "h2" | "h3" | "h4";
|
|
9
|
+
subtitleAs?: "h2" | "h3" | "h4" | "h5" | "p";
|
|
9
10
|
isLoading?: boolean;
|
|
10
11
|
children: React.ReactNode;
|
|
11
12
|
headerActions?: React.ReactNode;
|
|
12
13
|
contentProps?: React.ComponentProps<typeof Box>;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
type StaticContentBlockProps = ContentBlockBaseProps & {
|
|
16
|
+
isCollapsible?: false;
|
|
17
|
+
isOpen?: never;
|
|
18
|
+
defaultOpen?: never;
|
|
19
|
+
onOpenChange?: never;
|
|
20
|
+
};
|
|
21
|
+
type CollapsibleContentBlockProps = ContentBlockBaseProps & {
|
|
22
|
+
/** Enables collapse/expand behavior on the content area */
|
|
23
|
+
isCollapsible: true;
|
|
24
|
+
/** Controlled open state. When provided, the component is in controlled mode. */
|
|
25
|
+
isOpen?: boolean;
|
|
26
|
+
/** Initial open state for uncontrolled mode. Defaults to true. */
|
|
27
|
+
defaultOpen?: boolean;
|
|
28
|
+
/** Called when the open state changes */
|
|
29
|
+
onOpenChange?: (open: boolean) => void;
|
|
30
|
+
};
|
|
31
|
+
type ContentBlockProps = StaticContentBlockProps | CollapsibleContentBlockProps;
|
|
32
|
+
declare const ContentBlock: ({ isCollapsible, ...props }: ContentBlockProps) => react_jsx_runtime.JSX.Element;
|
|
15
33
|
|
|
16
34
|
export { ContentBlock, type ContentBlockProps, ContentBlock as default };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
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
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -37,30 +27,27 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
27
|
|
|
38
28
|
// src/ContentBlock.tsx
|
|
39
29
|
var import_react = require("react");
|
|
40
|
-
var import_seeds_react_box2 = require("@sproutsocial/seeds-react-box");
|
|
41
|
-
var import_seeds_react_text = require("@sproutsocial/seeds-react-text");
|
|
42
|
-
var import_seeds_react_loader = require("@sproutsocial/seeds-react-loader");
|
|
43
|
-
|
|
44
|
-
// src/styles.ts
|
|
45
|
-
var import_styled_components = __toESM(require("styled-components"));
|
|
46
30
|
var import_seeds_react_box = require("@sproutsocial/seeds-react-box");
|
|
47
|
-
var
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
align-items: center;
|
|
51
|
-
border-bottom: ${({ theme }) => theme.borderWidths[500]} solid
|
|
52
|
-
${({ theme }) => theme.colors.container.border.base};
|
|
53
|
-
color: ${({ theme }) => theme.colors.text.headline};
|
|
54
|
-
padding: ${({ theme }) => theme.space[400]};
|
|
55
|
-
`;
|
|
56
|
-
var ContentBlockContent = (0, import_styled_components.default)(import_seeds_react_box.Box)`
|
|
57
|
-
padding: ${({ theme }) => theme.space[400]};
|
|
58
|
-
`;
|
|
59
|
-
|
|
60
|
-
// src/ContentBlock.tsx
|
|
31
|
+
var import_seeds_react_loader = require("@sproutsocial/seeds-react-loader");
|
|
32
|
+
var import_seeds_react_collapsible = require("@sproutsocial/seeds-react-collapsible");
|
|
33
|
+
var import_seeds_react_content_header = require("@sproutsocial/seeds-react-content-header");
|
|
61
34
|
var import_seeds_react_container = require("@sproutsocial/seeds-react-container");
|
|
62
35
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
63
|
-
var
|
|
36
|
+
var ContentArea = ({
|
|
37
|
+
isLoading,
|
|
38
|
+
children,
|
|
39
|
+
contentProps
|
|
40
|
+
}) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
41
|
+
import_seeds_react_box.Box,
|
|
42
|
+
{
|
|
43
|
+
p: 400,
|
|
44
|
+
...contentProps,
|
|
45
|
+
borderBottomLeftRadius: "outer",
|
|
46
|
+
borderBottomRightRadius: "outer",
|
|
47
|
+
children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_box.Box, { my: 400, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_loader.Loader, { delay: false }) }) : children
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
var CollapsibleContentBlock = ({
|
|
64
51
|
title,
|
|
65
52
|
subtitle,
|
|
66
53
|
titleAs = "h2",
|
|
@@ -69,26 +56,60 @@ var ContentBlock = ({
|
|
|
69
56
|
isLoading,
|
|
70
57
|
headerActions,
|
|
71
58
|
contentProps,
|
|
72
|
-
|
|
59
|
+
isOpen: controlledIsOpen,
|
|
60
|
+
defaultOpen = true,
|
|
61
|
+
onOpenChange
|
|
73
62
|
}) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
63
|
+
const [internalOpen, setInternalOpen] = (0, import_react.useState)(defaultOpen);
|
|
64
|
+
const open = controlledIsOpen !== void 0 ? controlledIsOpen : internalOpen;
|
|
65
|
+
const handleOpenChange = (newOpen) => {
|
|
66
|
+
if (controlledIsOpen === void 0) {
|
|
67
|
+
setInternalOpen(newOpen);
|
|
68
|
+
}
|
|
69
|
+
onOpenChange?.(newOpen);
|
|
70
|
+
};
|
|
71
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_collapsible.Collapsible, { isOpen: open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_seeds_react_container.Container, { as: "section", children: [
|
|
72
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
73
|
+
import_seeds_react_content_header.CollapsibleContentHeader,
|
|
74
|
+
{
|
|
75
|
+
title,
|
|
76
|
+
subtitle,
|
|
77
|
+
headingLevel: titleAs,
|
|
78
|
+
subtitleAs,
|
|
79
|
+
headerActions,
|
|
80
|
+
trigger: import_seeds_react_collapsible.Collapsible.Trigger,
|
|
81
|
+
triggerPosition: "left"
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_collapsible.Collapsible.Panel, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContentArea, { isLoading, contentProps, children }) })
|
|
85
|
+
] }) });
|
|
86
|
+
};
|
|
87
|
+
var ContentBlock = ({ isCollapsible, ...props }) => {
|
|
88
|
+
if (isCollapsible) {
|
|
89
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleContentBlock, { ...props });
|
|
90
|
+
}
|
|
91
|
+
const {
|
|
92
|
+
title,
|
|
93
|
+
subtitle,
|
|
94
|
+
titleAs = "h2",
|
|
95
|
+
subtitleAs = "h3",
|
|
96
|
+
children,
|
|
97
|
+
isLoading,
|
|
98
|
+
headerActions,
|
|
99
|
+
contentProps
|
|
100
|
+
} = props;
|
|
101
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_seeds_react_container.Container, { as: "section", children: [
|
|
82
102
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
83
|
-
|
|
103
|
+
import_seeds_react_content_header.ContentHeader,
|
|
84
104
|
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
title,
|
|
106
|
+
subtitle,
|
|
107
|
+
headingLevel: titleAs,
|
|
108
|
+
subtitleAs,
|
|
109
|
+
headerActions
|
|
90
110
|
}
|
|
91
|
-
)
|
|
111
|
+
),
|
|
112
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContentArea, { isLoading, contentProps, children })
|
|
92
113
|
] });
|
|
93
114
|
};
|
|
94
115
|
var ContentBlock_default = ContentBlock;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/ContentBlock.tsx"
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ContentBlock.tsx"],"sourcesContent":["import ContentBlock from \"./ContentBlock\";\n\nexport type { ContentBlockProps } from \"./ContentBlock\";\n\nexport default ContentBlock;\nexport { ContentBlock };\n","import React, { useState } from \"react\";\nimport { Box } from \"@sproutsocial/seeds-react-box\";\nimport { Loader } from \"@sproutsocial/seeds-react-loader\";\nimport { Collapsible } from \"@sproutsocial/seeds-react-collapsible\";\nimport {\n ContentHeader,\n CollapsibleContentHeader,\n} from \"@sproutsocial/seeds-react-content-header\";\nimport { Container } from \"@sproutsocial/seeds-react-container\";\n\ninterface ContentBlockBaseProps {\n title: string;\n subtitle: string;\n titleAs?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n subtitleAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"p\";\n isLoading?: boolean;\n children: React.ReactNode;\n headerActions?: React.ReactNode;\n contentProps?: React.ComponentProps<typeof Box>;\n}\n\ntype StaticContentBlockProps = ContentBlockBaseProps & {\n isCollapsible?: false;\n isOpen?: never;\n defaultOpen?: never;\n onOpenChange?: never;\n};\n\ntype CollapsibleContentBlockProps = ContentBlockBaseProps & {\n /** Enables collapse/expand behavior on the content area */\n isCollapsible: true;\n /** Controlled open state. When provided, the component is in controlled mode. */\n isOpen?: boolean;\n /** Initial open state for uncontrolled mode. Defaults to true. */\n defaultOpen?: boolean;\n /** Called when the open state changes */\n onOpenChange?: (open: boolean) => void;\n};\n\nexport type ContentBlockProps =\n | StaticContentBlockProps\n | CollapsibleContentBlockProps;\n\nconst ContentArea = ({\n isLoading,\n children,\n contentProps,\n}: Pick<ContentBlockBaseProps, \"isLoading\" | \"children\" | \"contentProps\">) => (\n <Box\n p={400}\n {...contentProps}\n borderBottomLeftRadius=\"outer\"\n borderBottomRightRadius=\"outer\"\n >\n {isLoading ? (\n <Box my={400}>\n <Loader delay={false} />\n </Box>\n ) : (\n children\n )}\n </Box>\n);\n\nconst CollapsibleContentBlock = ({\n title,\n subtitle,\n titleAs = \"h2\",\n subtitleAs = \"h3\",\n children,\n isLoading,\n headerActions,\n contentProps,\n isOpen: controlledIsOpen,\n defaultOpen = true,\n onOpenChange,\n}: Omit<CollapsibleContentBlockProps, \"isCollapsible\">) => {\n const [internalOpen, setInternalOpen] = useState(defaultOpen);\n const open = controlledIsOpen !== undefined ? controlledIsOpen : internalOpen;\n\n const handleOpenChange = (newOpen: boolean) => {\n if (controlledIsOpen === undefined) {\n setInternalOpen(newOpen);\n }\n onOpenChange?.(newOpen);\n };\n\n return (\n <Collapsible isOpen={open} onOpenChange={handleOpenChange}>\n <Container as=\"section\">\n <CollapsibleContentHeader\n title={title}\n subtitle={subtitle}\n headingLevel={titleAs}\n subtitleAs={subtitleAs}\n headerActions={headerActions}\n trigger={Collapsible.Trigger}\n triggerPosition=\"left\"\n />\n <Collapsible.Panel>\n <ContentArea isLoading={isLoading} contentProps={contentProps}>\n {children}\n </ContentArea>\n </Collapsible.Panel>\n </Container>\n </Collapsible>\n );\n};\n\nconst ContentBlock = ({ isCollapsible, ...props }: ContentBlockProps) => {\n if (isCollapsible) {\n return <CollapsibleContentBlock {...props} />;\n }\n\n const {\n title,\n subtitle,\n titleAs = \"h2\",\n subtitleAs = \"h3\",\n children,\n isLoading,\n headerActions,\n contentProps,\n } = props;\n\n return (\n <Container as=\"section\">\n <ContentHeader\n title={title}\n subtitle={subtitle}\n headingLevel={titleAs}\n subtitleAs={subtitleAs}\n headerActions={headerActions}\n />\n <ContentArea isLoading={isLoading} contentProps={contentProps}>\n {children}\n </ContentArea>\n </Container>\n );\n};\n\nexport default ContentBlock;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAgC;AAChC,6BAAoB;AACpB,gCAAuB;AACvB,qCAA4B;AAC5B,wCAGO;AACP,mCAA0B;AAgDlB;AAbR,IAAM,cAAc,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF,MACE;AAAA,EAAC;AAAA;AAAA,IACC,GAAG;AAAA,IACF,GAAG;AAAA,IACJ,wBAAuB;AAAA,IACvB,yBAAwB;AAAA,IAEvB,sBACC,4CAAC,8BAAI,IAAI,KACP,sDAAC,oCAAO,OAAO,OAAO,GACxB,IAEA;AAAA;AAEJ;AAGF,IAAM,0BAA0B,CAAC;AAAA,EAC/B;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,cAAc;AAAA,EACd;AACF,MAA2D;AACzD,QAAM,CAAC,cAAc,eAAe,QAAI,uBAAS,WAAW;AAC5D,QAAM,OAAO,qBAAqB,SAAY,mBAAmB;AAEjE,QAAM,mBAAmB,CAAC,YAAqB;AAC7C,QAAI,qBAAqB,QAAW;AAClC,sBAAgB,OAAO;AAAA,IACzB;AACA,mBAAe,OAAO;AAAA,EACxB;AAEA,SACE,4CAAC,8CAAY,QAAQ,MAAM,cAAc,kBACvC,uDAAC,0CAAU,IAAG,WACZ;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,cAAc;AAAA,QACd;AAAA,QACA;AAAA,QACA,SAAS,2CAAY;AAAA,QACrB,iBAAgB;AAAA;AAAA,IAClB;AAAA,IACA,4CAAC,2CAAY,OAAZ,EACC,sDAAC,eAAY,WAAsB,cAChC,UACH,GACF;AAAA,KACF,GACF;AAEJ;AAEA,IAAM,eAAe,CAAC,EAAE,eAAe,GAAG,MAAM,MAAyB;AACvE,MAAI,eAAe;AACjB,WAAO,4CAAC,2BAAyB,GAAG,OAAO;AAAA,EAC7C;AAEA,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,SACE,6CAAC,0CAAU,IAAG,WACZ;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,cAAc;AAAA,QACd;AAAA,QACA;AAAA;AAAA,IACF;AAAA,IACA,4CAAC,eAAY,WAAsB,cAChC,UACH;AAAA,KACF;AAEJ;AAEA,IAAO,uBAAQ;;;ADzIf,IAAO,gBAAQ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-content-block",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Seeds React ContentBlock",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,14 +18,13 @@
|
|
|
18
18
|
"test:watch": "jest --watch --coverage=false"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@sproutsocial/seeds-react-box": "^1.1.
|
|
22
|
-
"@sproutsocial/seeds-react-button": "^1.
|
|
23
|
-
"@sproutsocial/seeds-react-
|
|
24
|
-
"@sproutsocial/seeds-react-
|
|
25
|
-
"@sproutsocial/seeds-react-
|
|
26
|
-
"@sproutsocial/seeds-react-
|
|
27
|
-
"@sproutsocial/seeds-react-
|
|
28
|
-
"@sproutsocial/seeds-react-theme": "^3.5.0"
|
|
21
|
+
"@sproutsocial/seeds-react-box": "^1.1.14",
|
|
22
|
+
"@sproutsocial/seeds-react-button": "^1.4.0",
|
|
23
|
+
"@sproutsocial/seeds-react-collapsible": "^2.0.0",
|
|
24
|
+
"@sproutsocial/seeds-react-container": "^0.3.8",
|
|
25
|
+
"@sproutsocial/seeds-react-content-header": "^0.2.1",
|
|
26
|
+
"@sproutsocial/seeds-react-loader": "^1.0.14",
|
|
27
|
+
"@sproutsocial/seeds-react-theme": "^3.6.0"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|
|
31
30
|
"@types/react": "^18.0.0",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
1
2
|
import ContentBlock from "./ContentBlock";
|
|
2
3
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
4
|
import { Text } from "@sproutsocial/seeds-react-text";
|
|
@@ -50,3 +51,70 @@ export const WithContentStyles: Story = {
|
|
|
50
51
|
<ContentBlock {...args} contentProps={{ p: 0, py: 400, bg: "blue.300" }} />
|
|
51
52
|
),
|
|
52
53
|
};
|
|
54
|
+
|
|
55
|
+
export const Collapsible: Story = {
|
|
56
|
+
render: (args) => (
|
|
57
|
+
<ContentBlock {...args} isCollapsible>
|
|
58
|
+
<Text.SmallBodyCopy>
|
|
59
|
+
This content can be collapsed and expanded by clicking the header.
|
|
60
|
+
</Text.SmallBodyCopy>
|
|
61
|
+
</ContentBlock>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const CollapsibleDefaultClosed: Story = {
|
|
66
|
+
name: "Collapsible (Default Closed)",
|
|
67
|
+
render: (args) => (
|
|
68
|
+
<ContentBlock {...args} isCollapsible defaultOpen={false}>
|
|
69
|
+
<Text.SmallBodyCopy>
|
|
70
|
+
This content starts collapsed and can be expanded by clicking the
|
|
71
|
+
header.
|
|
72
|
+
</Text.SmallBodyCopy>
|
|
73
|
+
</ContentBlock>
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const CollapsibleWithHeaderActions: Story = {
|
|
78
|
+
name: "Collapsible with Header Actions",
|
|
79
|
+
render: (args) => (
|
|
80
|
+
<ContentBlock
|
|
81
|
+
{...args}
|
|
82
|
+
isCollapsible
|
|
83
|
+
headerActions={
|
|
84
|
+
<Button size="small">
|
|
85
|
+
<Icon name="face-frown-open-solid" />
|
|
86
|
+
</Button>
|
|
87
|
+
}
|
|
88
|
+
>
|
|
89
|
+
<Text.SmallBodyCopy>
|
|
90
|
+
The header actions are independent from the collapse trigger. Clicking
|
|
91
|
+
the action button does not toggle the content.
|
|
92
|
+
</Text.SmallBodyCopy>
|
|
93
|
+
</ContentBlock>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const CollapsibleControlled: Story = {
|
|
98
|
+
name: "Collapsible (Controlled)",
|
|
99
|
+
render: (args) => {
|
|
100
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<Button onClick={() => setIsOpen(!isOpen)} mb={400}>
|
|
104
|
+
{isOpen ? "Close" : "Open"} externally
|
|
105
|
+
</Button>
|
|
106
|
+
<ContentBlock
|
|
107
|
+
{...args}
|
|
108
|
+
isCollapsible
|
|
109
|
+
isOpen={isOpen}
|
|
110
|
+
onOpenChange={setIsOpen}
|
|
111
|
+
>
|
|
112
|
+
<Text.SmallBodyCopy>
|
|
113
|
+
This content block is controlled externally. Use the button above or
|
|
114
|
+
click the header to toggle.
|
|
115
|
+
</Text.SmallBodyCopy>
|
|
116
|
+
</ContentBlock>
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
};
|
package/src/ContentBlock.tsx
CHANGED
|
@@ -1,22 +1,68 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
|
-
import { Text } from "@sproutsocial/seeds-react-text";
|
|
4
3
|
import { Loader } from "@sproutsocial/seeds-react-loader";
|
|
5
|
-
import {
|
|
4
|
+
import { Collapsible } from "@sproutsocial/seeds-react-collapsible";
|
|
5
|
+
import {
|
|
6
|
+
ContentHeader,
|
|
7
|
+
CollapsibleContentHeader,
|
|
8
|
+
} from "@sproutsocial/seeds-react-content-header";
|
|
6
9
|
import { Container } from "@sproutsocial/seeds-react-container";
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
interface ContentBlockBaseProps {
|
|
9
12
|
title: string;
|
|
10
13
|
subtitle: string;
|
|
11
|
-
titleAs
|
|
12
|
-
subtitleAs
|
|
14
|
+
titleAs?: "h1" | "h2" | "h3" | "h4";
|
|
15
|
+
subtitleAs?: "h2" | "h3" | "h4" | "h5" | "p";
|
|
13
16
|
isLoading?: boolean;
|
|
14
17
|
children: React.ReactNode;
|
|
15
18
|
headerActions?: React.ReactNode;
|
|
16
19
|
contentProps?: React.ComponentProps<typeof Box>;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
type StaticContentBlockProps = ContentBlockBaseProps & {
|
|
23
|
+
isCollapsible?: false;
|
|
24
|
+
isOpen?: never;
|
|
25
|
+
defaultOpen?: never;
|
|
26
|
+
onOpenChange?: never;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type CollapsibleContentBlockProps = ContentBlockBaseProps & {
|
|
30
|
+
/** Enables collapse/expand behavior on the content area */
|
|
31
|
+
isCollapsible: true;
|
|
32
|
+
/** Controlled open state. When provided, the component is in controlled mode. */
|
|
33
|
+
isOpen?: boolean;
|
|
34
|
+
/** Initial open state for uncontrolled mode. Defaults to true. */
|
|
35
|
+
defaultOpen?: boolean;
|
|
36
|
+
/** Called when the open state changes */
|
|
37
|
+
onOpenChange?: (open: boolean) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ContentBlockProps =
|
|
41
|
+
| StaticContentBlockProps
|
|
42
|
+
| CollapsibleContentBlockProps;
|
|
43
|
+
|
|
44
|
+
const ContentArea = ({
|
|
45
|
+
isLoading,
|
|
46
|
+
children,
|
|
47
|
+
contentProps,
|
|
48
|
+
}: Pick<ContentBlockBaseProps, "isLoading" | "children" | "contentProps">) => (
|
|
49
|
+
<Box
|
|
50
|
+
p={400}
|
|
51
|
+
{...contentProps}
|
|
52
|
+
borderBottomLeftRadius="outer"
|
|
53
|
+
borderBottomRightRadius="outer"
|
|
54
|
+
>
|
|
55
|
+
{isLoading ? (
|
|
56
|
+
<Box my={400}>
|
|
57
|
+
<Loader delay={false} />
|
|
58
|
+
</Box>
|
|
59
|
+
) : (
|
|
60
|
+
children
|
|
61
|
+
)}
|
|
62
|
+
</Box>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const CollapsibleContentBlock = ({
|
|
20
66
|
title,
|
|
21
67
|
subtitle,
|
|
22
68
|
titleAs = "h2",
|
|
@@ -25,33 +71,70 @@ const ContentBlock: React.FC<ContentBlockProps> = ({
|
|
|
25
71
|
isLoading,
|
|
26
72
|
headerActions,
|
|
27
73
|
contentProps,
|
|
28
|
-
|
|
29
|
-
|
|
74
|
+
isOpen: controlledIsOpen,
|
|
75
|
+
defaultOpen = true,
|
|
76
|
+
onOpenChange,
|
|
77
|
+
}: Omit<CollapsibleContentBlockProps, "isCollapsible">) => {
|
|
78
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
79
|
+
const open = controlledIsOpen !== undefined ? controlledIsOpen : internalOpen;
|
|
80
|
+
|
|
81
|
+
const handleOpenChange = (newOpen: boolean) => {
|
|
82
|
+
if (controlledIsOpen === undefined) {
|
|
83
|
+
setInternalOpen(newOpen);
|
|
84
|
+
}
|
|
85
|
+
onOpenChange?.(newOpen);
|
|
86
|
+
};
|
|
87
|
+
|
|
30
88
|
return (
|
|
31
|
-
<
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
89
|
+
<Collapsible isOpen={open} onOpenChange={handleOpenChange}>
|
|
90
|
+
<Container as="section">
|
|
91
|
+
<CollapsibleContentHeader
|
|
92
|
+
title={title}
|
|
93
|
+
subtitle={subtitle}
|
|
94
|
+
headingLevel={titleAs}
|
|
95
|
+
subtitleAs={subtitleAs}
|
|
96
|
+
headerActions={headerActions}
|
|
97
|
+
trigger={Collapsible.Trigger}
|
|
98
|
+
triggerPosition="left"
|
|
99
|
+
/>
|
|
100
|
+
<Collapsible.Panel>
|
|
101
|
+
<ContentArea isLoading={isLoading} contentProps={contentProps}>
|
|
102
|
+
{children}
|
|
103
|
+
</ContentArea>
|
|
104
|
+
</Collapsible.Panel>
|
|
105
|
+
</Container>
|
|
106
|
+
</Collapsible>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const ContentBlock = ({ isCollapsible, ...props }: ContentBlockProps) => {
|
|
111
|
+
if (isCollapsible) {
|
|
112
|
+
return <CollapsibleContentBlock {...props} />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const {
|
|
116
|
+
title,
|
|
117
|
+
subtitle,
|
|
118
|
+
titleAs = "h2",
|
|
119
|
+
subtitleAs = "h3",
|
|
120
|
+
children,
|
|
121
|
+
isLoading,
|
|
122
|
+
headerActions,
|
|
123
|
+
contentProps,
|
|
124
|
+
} = props;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Container as="section">
|
|
128
|
+
<ContentHeader
|
|
129
|
+
title={title}
|
|
130
|
+
subtitle={subtitle}
|
|
131
|
+
headingLevel={titleAs}
|
|
132
|
+
subtitleAs={subtitleAs}
|
|
133
|
+
headerActions={headerActions}
|
|
134
|
+
/>
|
|
135
|
+
<ContentArea isLoading={isLoading} contentProps={contentProps}>
|
|
136
|
+
{children}
|
|
137
|
+
</ContentArea>
|
|
55
138
|
</Container>
|
|
56
139
|
);
|
|
57
140
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
fireEvent,
|
|
6
|
+
} from "@sproutsocial/seeds-react-testing-library";
|
|
2
7
|
import ContentBlock from "../ContentBlock";
|
|
3
8
|
|
|
4
9
|
describe("ContentBlock Features", () => {
|
|
@@ -56,3 +61,150 @@ describe("ContentBlock Features", () => {
|
|
|
56
61
|
).toBeInTheDocument();
|
|
57
62
|
});
|
|
58
63
|
});
|
|
64
|
+
|
|
65
|
+
describe("ContentBlock Collapsible", () => {
|
|
66
|
+
test("does not render chevron when not collapsible", () => {
|
|
67
|
+
render(
|
|
68
|
+
<ContentBlock title="Title" subtitle="Subtitle">
|
|
69
|
+
Content
|
|
70
|
+
</ContentBlock>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("renders chevron icon when collapsible", () => {
|
|
77
|
+
render(
|
|
78
|
+
<ContentBlock title="Title" subtitle="Subtitle" isCollapsible>
|
|
79
|
+
Content
|
|
80
|
+
</ContentBlock>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const trigger = screen.getByRole("button");
|
|
84
|
+
expect(trigger).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("defaults to open when collapsible", () => {
|
|
88
|
+
render(
|
|
89
|
+
<ContentBlock title="Title" subtitle="Subtitle" isCollapsible>
|
|
90
|
+
Visible Content
|
|
91
|
+
</ContentBlock>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const trigger = screen.getByRole("button");
|
|
95
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
96
|
+
expect(screen.getByText("Visible Content")).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("can default to closed", () => {
|
|
100
|
+
render(
|
|
101
|
+
<ContentBlock
|
|
102
|
+
title="Title"
|
|
103
|
+
subtitle="Subtitle"
|
|
104
|
+
isCollapsible
|
|
105
|
+
defaultOpen={false}
|
|
106
|
+
>
|
|
107
|
+
Hidden Content
|
|
108
|
+
</ContentBlock>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const trigger = screen.getByRole("button");
|
|
112
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("toggles content on trigger click", () => {
|
|
116
|
+
render(
|
|
117
|
+
<ContentBlock title="Title" subtitle="Subtitle" isCollapsible>
|
|
118
|
+
Toggle Content
|
|
119
|
+
</ContentBlock>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const trigger = screen.getByRole("button");
|
|
123
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
124
|
+
|
|
125
|
+
fireEvent.click(trigger);
|
|
126
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
127
|
+
|
|
128
|
+
fireEvent.click(trigger);
|
|
129
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("controlled mode respects isOpen prop", () => {
|
|
133
|
+
const ControlledContentBlock = () => {
|
|
134
|
+
const [open, setOpen] = useState(false);
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<button onClick={() => setOpen(!open)}>External Toggle</button>
|
|
138
|
+
<ContentBlock
|
|
139
|
+
title="Title"
|
|
140
|
+
subtitle="Subtitle"
|
|
141
|
+
isCollapsible
|
|
142
|
+
isOpen={open}
|
|
143
|
+
onOpenChange={setOpen}
|
|
144
|
+
>
|
|
145
|
+
Controlled Content
|
|
146
|
+
</ContentBlock>
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
render(<ControlledContentBlock />);
|
|
152
|
+
|
|
153
|
+
const trigger = screen.getByRole("button", { name: /External Toggle/i });
|
|
154
|
+
const collapsibleTrigger = screen
|
|
155
|
+
.getAllByRole("button")
|
|
156
|
+
.find((btn) => btn.getAttribute("aria-expanded") !== null);
|
|
157
|
+
|
|
158
|
+
expect(collapsibleTrigger).toHaveAttribute("aria-expanded", "false");
|
|
159
|
+
|
|
160
|
+
fireEvent.click(trigger);
|
|
161
|
+
expect(collapsibleTrigger).toHaveAttribute("aria-expanded", "true");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("headerActions render alongside trigger and do not toggle collapse", () => {
|
|
165
|
+
const actionClick = jest.fn();
|
|
166
|
+
|
|
167
|
+
render(
|
|
168
|
+
<ContentBlock
|
|
169
|
+
title="Title"
|
|
170
|
+
subtitle="Subtitle"
|
|
171
|
+
isCollapsible
|
|
172
|
+
headerActions={<button onClick={actionClick}>Action</button>}
|
|
173
|
+
>
|
|
174
|
+
Content
|
|
175
|
+
</ContentBlock>
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const actionButton = screen.getByText("Action");
|
|
179
|
+
expect(actionButton).toBeInTheDocument();
|
|
180
|
+
|
|
181
|
+
const collapsibleTrigger = screen
|
|
182
|
+
.getAllByRole("button")
|
|
183
|
+
.find((btn) => btn.getAttribute("aria-expanded") !== null);
|
|
184
|
+
expect(collapsibleTrigger).toHaveAttribute("aria-expanded", "true");
|
|
185
|
+
|
|
186
|
+
fireEvent.click(actionButton);
|
|
187
|
+
expect(actionClick).toHaveBeenCalledTimes(1);
|
|
188
|
+
// Collapse state should not change from clicking the action
|
|
189
|
+
expect(collapsibleTrigger).toHaveAttribute("aria-expanded", "true");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("calls onOpenChange when toggled", () => {
|
|
193
|
+
const onOpenChange = jest.fn();
|
|
194
|
+
|
|
195
|
+
render(
|
|
196
|
+
<ContentBlock
|
|
197
|
+
title="Title"
|
|
198
|
+
subtitle="Subtitle"
|
|
199
|
+
isCollapsible
|
|
200
|
+
onOpenChange={onOpenChange}
|
|
201
|
+
>
|
|
202
|
+
Content
|
|
203
|
+
</ContentBlock>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const trigger = screen.getByRole("button");
|
|
207
|
+
fireEvent.click(trigger);
|
|
208
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
package/src/styles.ts
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import styled from "styled-components";
|
|
2
2
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
3
3
|
|
|
4
|
-
export const ContentBlockHeader = styled(Box)`
|
|
5
|
-
display: flex;
|
|
6
|
-
justify-content: space-between;
|
|
7
|
-
align-items: center;
|
|
8
|
-
border-bottom: ${({ theme }) => theme.borderWidths[500]} solid
|
|
9
|
-
${({ theme }) => theme.colors.container.border.base};
|
|
10
|
-
color: ${({ theme }) => theme.colors.text.headline};
|
|
11
|
-
padding: ${({ theme }) => theme.space[400]};
|
|
12
|
-
`;
|
|
13
|
-
|
|
14
4
|
export const ContentBlockContent = styled(Box)`
|
|
15
5
|
padding: ${({ theme }) => theme.space[400]};
|
|
16
6
|
`;
|