Skip to content

Commit d9ee13a

Browse files
authored
Merge pull request #240 from eccenca/feature/content-group-CMEM-5979
Add ContentGroup component (CMEM-5979)
2 parents 55491bf + 5b7ad54 commit d9ee13a

File tree

8 files changed

+368
-2
lines changed

8 files changed

+368
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
88

99
### Added
1010

11+
- `CntentGroup` component
12+
- Manage display of a grouped content section.
13+
- Add info, actions and context annotations by using its properties.
14+
- Can be nested into each other.
1115
- `<CodeEditor />`
1216
- implemented support for linting which is enabled via `useLinting` prop
1317
- `turtle` and `javascript` are currently supported languages for linting
@@ -18,7 +22,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1822
### Fixed
1923

2024
- `MultiSelect`:
21-
- Old suggestions might be shown for a very short time when typing in a new search query.
25+
- Old suggestions might be shown for a very short time when typing in a new search query.
2226

2327
### Changed
2428

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"@codemirror/lang-xml": "^6.1.0",
7979
"@codemirror/legacy-modes": "^6.4.2",
8080
"@mavrin/remark-typograf": "^2.2.0",
81+
"classnames": "^2.5.1",
8182
"codemirror": "^6.0.1",
8283
"color": "^4.2.3",
8384
"compute-scroll-into-view": "^3.1.0",
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from "react";
2+
import { LoremIpsum } from "react-lorem-ipsum";
3+
import { Meta, StoryFn } from "@storybook/react";
4+
5+
import { Badge, ContentGroup, HtmlContentBlock, IconButton, Tag } from "../../../index";
6+
7+
export default {
8+
title: "Components/ContentGroup",
9+
component: ContentGroup,
10+
argTypes: {
11+
handlerToggleCollapse: {
12+
action: "toggle collapse",
13+
},
14+
},
15+
} as Meta<typeof ContentGroup>;
16+
17+
const TemplateFull: StoryFn<typeof ContentGroup> = (args) => <ContentGroup {...args} />;
18+
19+
export const BasicExample = TemplateFull.bind({});
20+
BasicExample.args = {
21+
title: "Content group title",
22+
contextInfo: <Badge children={100} maxLength={3} intent={"warning"} title="Found warnings context." />,
23+
annotation: (
24+
<Tag backgroundColor={"purple"} round>
25+
Context tag
26+
</Tag>
27+
),
28+
actionOptions: (
29+
<>
30+
<IconButton name="item-remove" text="Example remove tooltip" disruptive />
31+
</>
32+
),
33+
isCollapsed: false,
34+
handlerToggleCollapse: () => {},
35+
borderMainConnection: true,
36+
borderSubConnection: ["red", "blue"],
37+
level: 1,
38+
minimumHeadlineLevel: 5,
39+
whitespaceSize: "small",
40+
description: "More context description by tooltip.",
41+
hideGroupDivider: false,
42+
children: (
43+
<HtmlContentBlock>
44+
<LoremIpsum p={3} avgSentencesPerParagraph={4} random={false} />
45+
</HtmlContentBlock>
46+
),
47+
};
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import React from "react";
2+
import classNames from "classnames";
3+
import Color from "color";
4+
5+
import { TestableComponent } from "../../components/interfaces";
6+
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
7+
import {
8+
Divider,
9+
Icon,
10+
IconButton,
11+
OverflowText,
12+
Section,
13+
SectionHeader,
14+
Spacing,
15+
StickyTarget,
16+
StickyTargetProps,
17+
Toolbar,
18+
ToolbarSection,
19+
Tooltip,
20+
} from "../index";
21+
22+
export interface ContentGroupProps extends Omit<React.HTMLAttributes<HTMLElement>, "title">, TestableComponent {
23+
/**
24+
* Title of the content group.
25+
*/
26+
title?: string;
27+
/**
28+
* Level of the content group.
29+
*/
30+
level?: number;
31+
/**
32+
* Context information to display in the header.
33+
*/
34+
contextInfo?: React.ReactElement | React.ReactElement[];
35+
/**
36+
* Annotation to display in the content.
37+
*/
38+
annotation?: React.ReactElement | React.ReactElement[];
39+
/**
40+
* Action options to display in the header.
41+
*/
42+
actionOptions?: React.ReactElement | React.ReactElement[];
43+
/**
44+
* Flag to collapse the content group.
45+
*/
46+
isCollapsed?: boolean;
47+
/**
48+
* Text to display when the callapse button is hovered.
49+
* If not set then it uses "Show more" or "Show less".
50+
*/
51+
textToggleCollapse?: string;
52+
/**
53+
* Event handler to toggle the collapse state.
54+
*/
55+
handlerToggleCollapse?: () => void;
56+
/**
57+
* Use a border on the left side to visually connect the whole content content group.
58+
*/
59+
borderMainConnection?: boolean;
60+
/**
61+
* Use a border on the left side to visually emphase the content group.
62+
* If it is set to an array of color codes then the border is multi colored.
63+
*/
64+
borderSubConnection?: boolean | string[];
65+
/**
66+
* Whitespace size between header and the content.
67+
*/
68+
whitespaceSize?: "tiny" | "small" | "medium" | "large" | "xlarge";
69+
/**
70+
* Title minimum headline level.
71+
*/
72+
minimumHeadlineLevel?: 1 | 2 | 3 | 4 | 5 | 6;
73+
/**
74+
* Props to pass to `StickyTarget`.
75+
*/
76+
stickyHeaderProps?: Omit<StickyTargetProps, "children">;
77+
/**
78+
* Description of the content group.
79+
* Added as tooltip to an info icon placed in the content group header.
80+
*/
81+
description?: string;
82+
/**
83+
* Flag to hide the group divider.
84+
*/
85+
hideGroupDivider?: boolean;
86+
/**
87+
* Additional props to pass to the content container.
88+
*/
89+
contentProps?: Omit<React.HTMLAttributes<HTMLDivElement>, "children">;
90+
}
91+
92+
/**
93+
* Manage display of a grouped content section.
94+
* Add info, actions and context annotations by using its properties.
95+
* Can be nested into each other.
96+
*/
97+
export const ContentGroup = ({
98+
children,
99+
className = "",
100+
title,
101+
contextInfo,
102+
annotation,
103+
actionOptions,
104+
isCollapsed = false,
105+
textToggleCollapse,
106+
handlerToggleCollapse,
107+
borderMainConnection = false,
108+
borderSubConnection = false,
109+
level = 1,
110+
minimumHeadlineLevel = 3,
111+
whitespaceSize = "small",
112+
style,
113+
stickyHeaderProps,
114+
description,
115+
hideGroupDivider,
116+
contentProps,
117+
...otherContentWrapperProps
118+
}: ContentGroupProps) => {
119+
const displayHeader = title || handlerToggleCollapse;
120+
121+
let borderGradient: string[] | undefined = undefined;
122+
if (typeof borderSubConnection === "object") {
123+
const borderColors: string[] = Array.isArray(borderSubConnection) ? borderSubConnection : [borderSubConnection];
124+
borderGradient = borderColors.reduce((acc: string[], borderColor: string, index: number): string[] => {
125+
try {
126+
const color = Color(borderColor);
127+
128+
acc.push(
129+
`${color.rgb().toString()} ` +
130+
`${(index / borderColors.length) * 100}% ` +
131+
`${((index + 1) / borderColors.length) * 100}%`
132+
);
133+
} catch {
134+
// eslint-disable-next-line no-console
135+
console.warn("Received invalid background color for tag: " + borderColor);
136+
}
137+
return acc;
138+
}, []);
139+
}
140+
141+
const contextInfoElements = Array.isArray(contextInfo) ? contextInfo : [contextInfo];
142+
const { className: contentClassName, ...otherContentProps } = contentProps ?? {};
143+
144+
const headerContent = displayHeader ? (
145+
<>
146+
<SectionHeader className={`${eccgui}-contentgroup__header`}>
147+
<Toolbar>
148+
{handlerToggleCollapse && (
149+
<ToolbarSection>
150+
<IconButton
151+
className={`${eccgui}-contentgroup__header__toggler`}
152+
name={isCollapsed ? "toggler-showmore" : "toggler-showless"}
153+
text={textToggleCollapse ?? (isCollapsed ? "Show more" : "Show less")}
154+
onClick={handlerToggleCollapse}
155+
/>
156+
<Spacing vertical size="small" />
157+
</ToolbarSection>
158+
)}
159+
{title && (
160+
<ToolbarSection canShrink>
161+
{React.createElement(
162+
"h" +
163+
Math.min(
164+
Math.max(minimumHeadlineLevel, level + minimumHeadlineLevel),
165+
6
166+
).toString(),
167+
{
168+
children: <OverflowText>{title}</OverflowText>,
169+
className: `${eccgui}-contentgroup__header__title`,
170+
}
171+
)}
172+
{description && (
173+
<>
174+
<Spacing vertical size="tiny" />
175+
<Tooltip content={description}>
176+
<Icon name="item-info" small className="dmapp--text-info" />
177+
</Tooltip>
178+
</>
179+
)}
180+
</ToolbarSection>
181+
)}
182+
{contextInfoElements &&
183+
contextInfoElements[0]?.props &&
184+
Object.values(contextInfoElements[0].props).every((v) => v !== undefined) && (
185+
<ToolbarSection className={`${eccgui}-contentgroup__header__context`} canGrow>
186+
<div className={`${eccgui}-contentgroup__content `}>
187+
<Spacing vertical size="tiny" />
188+
{contextInfoElements}
189+
</div>
190+
</ToolbarSection>
191+
)}
192+
{!isCollapsed && handlerToggleCollapse && actionOptions && (
193+
<ToolbarSection className={`${eccgui}-contentgroup__header__options`}>
194+
<Spacing vertical size="small" />
195+
{actionOptions}
196+
</ToolbarSection>
197+
)}
198+
</Toolbar>
199+
</SectionHeader>
200+
{(!isCollapsed || !handlerToggleCollapse) && (
201+
<>
202+
{!hideGroupDivider && <Divider addSpacing="small" />}
203+
<Spacing size={whitespaceSize} />
204+
</>
205+
)}
206+
</>
207+
) : (
208+
<></>
209+
);
210+
211+
return (
212+
<Section
213+
className={
214+
`${eccgui}-contentgroup` +
215+
(className ? ` ${className}` : "") +
216+
(whitespaceSize ? ` ${eccgui}-contentgroup--padding-${whitespaceSize}` : "") +
217+
(borderMainConnection ? ` ${eccgui}-contentgroup--border-main` : "") +
218+
(borderSubConnection ? ` ${eccgui}-contentgroup--border-sub` : "")
219+
}
220+
style={
221+
borderGradient
222+
? ({
223+
...(style ?? {}),
224+
[`--${eccgui}-color-contentgroup-border-sub`]: borderGradient.join(", "),
225+
} as React.CSSProperties)
226+
: style
227+
}
228+
{...otherContentWrapperProps}
229+
>
230+
{headerContent && stickyHeaderProps ? (
231+
<StickyTarget {...stickyHeaderProps}>{headerContent}</StickyTarget>
232+
) : (
233+
headerContent
234+
)}
235+
{(!isCollapsed || !handlerToggleCollapse) && (
236+
<>
237+
<div className={`${eccgui}-contentgroup__content`}>
238+
<div
239+
className={classNames(`${eccgui}-contentgroup__content__body`, contentClassName)}
240+
{...otherContentProps}
241+
>
242+
{children}
243+
</div>
244+
{contextInfo && !displayHeader && (
245+
<div className={`${eccgui}-contentgroup__content__context`}>{contextInfoElements}</div>
246+
)}
247+
{annotation && <div>{annotation}</div>}
248+
{actionOptions && !displayHeader && (
249+
<div className={`${eccgui}-contentgroup__content__options`}>{actionOptions}</div>
250+
)}
251+
</div>
252+
</>
253+
)}
254+
</Section>
255+
);
256+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
$eccgui-color-scontentgroup-border-main: rgba($eccgui-color-workspace-text, $eccgui-opacity-muted) !default;
2+
$eccgui-color-scontentgroup-border-sub: rgba($eccgui-color-workspace-text, $eccgui-opacity-disabled) !default;
3+
4+
.#{$eccgui}-contentgroup {
5+
--#{$eccgui}-color-contentgroup-border-main: #{$eccgui-color-scontentgroup-border-main};
6+
--#{$eccgui}-color-contentgroup-border-sub: #{$eccgui-color-scontentgroup-border-sub};
7+
}
8+
9+
.#{$eccgui}-contentgroup--border-main {
10+
border-left: 0.25 * $eccgui-size-block-whitespace solid
11+
var(--#{$eccgui}-color-contentgroup-border-main, #{$eccgui-color-scontentgroup-border-main});
12+
13+
&.#{$eccgui}-contentgroup--padding-small {
14+
padding-left: 0.5 * $eccgui-size-block-whitespace;
15+
}
16+
}
17+
18+
.#{$eccgui}-contentgroup--border-sub {
19+
position: relative;
20+
border-right: 0.25 * $eccgui-size-block-whitespace solid transparent;
21+
22+
&::after {
23+
position: absolute;
24+
top: 0;
25+
bottom: 0;
26+
left: 100%;
27+
width: 0.25 * $eccgui-size-block-whitespace;
28+
content: " ";
29+
background-color: var(--#{$eccgui}-color-contentgroup-border-sub, #{$eccgui-color-scontentgroup-border-sub});
30+
background-image: linear-gradient(to bottom, var(--#{$eccgui}-color-contentgroup-border-sub));
31+
}
32+
33+
&.#{$eccgui}-contentgroup--padding-small {
34+
padding-right: 0.5 * $eccgui-size-block-whitespace;
35+
}
36+
}
37+
38+
.#{$eccgui}-contentgroup--padding-small {
39+
+ .#{$eccgui}-contentgroup {
40+
margin-top: 0.5 * $eccgui-size-block-whitespace;
41+
}
42+
43+
> .#{$eccgui}-contentgroup__content {
44+
column-gap: 0.5 * $eccgui-size-block-whitespace;
45+
}
46+
}
47+
48+
.#{$eccgui}-contentgroup__content {
49+
display: flex;
50+
}
51+
52+
.#{$eccgui}-contentgroup__content__body {
53+
flex-grow: 1;
54+
flex-shrink: 1;
55+
width: 100%;
56+
}

src/components/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@
4141
@import "./Badge/badge";
4242
@import "./PropertyValuePair/propertyvalue";
4343
@import "./MultiSuggestField/multisuggestfield";
44+
@import "./ContentGroup/contentgroup";

0 commit comments

Comments
 (0)