Skip to content

Commit 0428508

Browse files
ECHOES-1118 New Layout.Sidebar.Footer.PromotionCard
1 parent 8bafe22 commit 0428508

File tree

7 files changed

+318
-63
lines changed

7 files changed

+318
-63
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Echoes React
3+
* Copyright (C) 2023-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import styled from '@emotion/styled';
22+
import { cssVar } from '~utils/design-tokens';
23+
import { BadgeVariety } from '../../components/badges/Badge';
24+
25+
export enum PromotedSectionVariety {
26+
Highlight = 'highlight',
27+
Neutral = 'neutral',
28+
}
29+
30+
export const BADGE_VARIETIES = {
31+
[PromotedSectionVariety.Highlight]: BadgeVariety.Highlight,
32+
[PromotedSectionVariety.Neutral]: BadgeVariety.Neutral,
33+
};
34+
35+
export const PROMOTED_SECTION_STYLES = {
36+
[PromotedSectionVariety.Highlight]: {
37+
'--promoted-section-background-color': cssVar('color-background-emphasis-weak-default'),
38+
'--promoted-section-border': `1px solid ${cssVar('color-border-emphasis-weak')}`,
39+
},
40+
41+
[PromotedSectionVariety.Neutral]: {
42+
'--promoted-section-background-color': cssVar('color-surface-default'),
43+
'--promoted-section-border': `1px solid ${cssVar('color-border-weak')}`,
44+
},
45+
};
46+
47+
export const PromotedSectionMainStyles = styled.div`
48+
background-color: var(--promoted-section-background-color);
49+
border: var(--promoted-section-border);
50+
border-radius: ${cssVar('border-radius-400')};
51+
box-shadow: ${cssVar('box-shadow-xsmall')};
52+
padding: ${cssVar('dimension-space-200')};
53+
`;
54+
PromotedSectionMainStyles.displayName = 'PromotedSectionMainStyles';
55+
56+
export const PromotedSectionTextAndActions = styled.div`
57+
align-items: flex-start;
58+
display: flex;
59+
flex: 1 0 0;
60+
flex-direction: column;
61+
gap: ${cssVar('dimension-space-150')};
62+
`;
63+
PromotedSectionTextAndActions.displayName = 'PromotedSectionTextAndActions';
64+
65+
export const PromotedSectionTextContainer = styled.div`
66+
align-items: flex-start;
67+
align-self: stretch;
68+
display: flex;
69+
flex-direction: column;
70+
gap: ${cssVar('dimension-space-100')};
71+
`;
72+
PromotedSectionTextContainer.displayName = 'PromotedSectionTextContainer';
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Echoes React
3+
* Copyright (C) 2023-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import styled from '@emotion/styled';
22+
import { forwardRef, useMemo } from 'react';
23+
import {
24+
BADGE_VARIETIES,
25+
PROMOTED_SECTION_STYLES,
26+
PromotedSectionMainStyles,
27+
PromotedSectionTextAndActions,
28+
PromotedSectionTextContainer,
29+
PromotedSectionVariety,
30+
} from '~common/components/PromotedSectionStyles';
31+
import { TextNode } from '~types/utils';
32+
import { cssVar } from '~utils/design-tokens';
33+
import { Badge, BadgeSize } from '../../../components/badges';
34+
import { Heading, HeadingSize, Text } from '../../typography';
35+
36+
export interface SidebarNavigationFooterPromotionCard {
37+
/**
38+
* The actions at the bottom should be instances of Button or StandaloneLink in a fragment.
39+
* They are wrapped in a ButtonGroup by this component.
40+
*/
41+
actions: React.ReactNode;
42+
43+
/**
44+
* The text to display on a badge above the header (optional).
45+
* No badge appears if this is undefined or the empty string `""`
46+
*/
47+
badgeText?: TextNode;
48+
49+
/**
50+
* CSS class name(s) to apply to the section (optional)
51+
*/
52+
className?: string;
53+
54+
/**
55+
* The header text for the section
56+
*/
57+
headerText: TextNode;
58+
59+
/**
60+
* The main text for the section
61+
*/
62+
text: TextNode;
63+
64+
/**
65+
* The variety: either PromotedSectionVariety.Highlight/'highlight' or PromotedSectionVariety.Neutral/'neutral'.
66+
* Defaults to PromotedSectionVariety.Neutral/'neutral' (optional)
67+
*/
68+
variety?: `${PromotedSectionVariety}`;
69+
}
70+
71+
export const SidebarNavigationFooterPromotionCard = forwardRef<
72+
HTMLDivElement,
73+
Readonly<SidebarNavigationFooterPromotionCard>
74+
>(
75+
(
76+
{
77+
actions,
78+
badgeText,
79+
className,
80+
headerText,
81+
text,
82+
variety = PromotedSectionVariety.Neutral,
83+
...otherProps
84+
},
85+
ref,
86+
) => {
87+
return (
88+
<StyledPromotedSectionMainStyles
89+
className={className}
90+
css={useMemo(() => PROMOTED_SECTION_STYLES[variety], [variety])}
91+
ref={ref}
92+
{...otherProps}>
93+
<PromotedSectionTextAndActions>
94+
<PromotedSectionTextContainer>
95+
{badgeText && (
96+
<Badge
97+
isHighContrast={variety === PromotedSectionVariety.Highlight}
98+
size={BadgeSize.Small}
99+
variety={BADGE_VARIETIES[variety]}>
100+
{badgeText}
101+
</Badge>
102+
)}
103+
104+
<Heading as="h2" hasMarginBottom={false} size={HeadingSize.Medium}>
105+
{headerText}
106+
</Heading>
107+
108+
<Text>{text}</Text>
109+
</PromotedSectionTextContainer>
110+
111+
{actions}
112+
</PromotedSectionTextAndActions>
113+
</StyledPromotedSectionMainStyles>
114+
);
115+
},
116+
);
117+
118+
SidebarNavigationFooterPromotionCard.displayName = 'SidebarNavigationFooterPromotionCard';
119+
120+
const StyledPromotedSectionMainStyles = styled(PromotedSectionMainStyles)`
121+
display: flex;
122+
flex-direction: column;
123+
gap: ${cssVar('dimension-space-100')};
124+
align-items: start;
125+
opacity: 1;
126+
/* step-end makes it appear at the very end, when the sidebar has reached is full size.
127+
* This prevents showing it resizing
128+
*/
129+
transition: opacity 0.1s step-end;
130+
131+
[data-sidebar-docked='false'] nav:not(:hover, :focus-within) & {
132+
opacity: 0;
133+
/* When closing, we want the opposite: it disappears immediately */
134+
transition: opacity 0s;
135+
}
136+
`;
137+
StyledPromotedSectionMainStyles.displayName = 'StyledPromotedSectionMainStyles';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Echoes React
3+
* Copyright (C) 2023-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import { screen } from '@testing-library/react';
22+
import { render } from '~common/helpers/test-utils';
23+
import { Button } from '../../../buttons';
24+
import { SidebarNavigationFooterPromotionCard } from '../SidebarNavigationFooterPromotionCard';
25+
26+
it('should render correctly', async () => {
27+
const { container } = render(
28+
<SidebarNavigationFooterPromotionCard
29+
actions={<Button>action</Button>}
30+
headerText="title"
31+
text="description"
32+
/>,
33+
);
34+
35+
expect(screen.getByRole('heading')).toHaveTextContent('title');
36+
expect(screen.getByText('description')).toBeInTheDocument();
37+
expect(screen.getByRole('button', { name: 'action' })).toBeInTheDocument();
38+
await expect(container).toHaveNoA11yViolations();
39+
});
40+
41+
it('should render correctly with a badge', () => {
42+
render(
43+
<SidebarNavigationFooterPromotionCard
44+
actions={<Button>action</Button>}
45+
badgeText="beta"
46+
headerText="title"
47+
text="description"
48+
/>,
49+
);
50+
51+
expect(screen.getByText('beta')).toBeInTheDocument();
52+
});

src/components/layout/sidebar-navigation/index.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import { SidebarNavigation as SidebarNavigationRoot } from './SidebarNavigation';
2222
import { SidebarNavigationAccordionItem } from './SidebarNavigationAccordionItem';
2323
import { SidebarNavigationBody } from './SidebarNavigationBody';
24+
import { SidebarNavigationFooterPromotionCard } from './SidebarNavigationFooterPromotionCard';
2425
import { SidebarNavigationGroup } from './SidebarNavigationGroup';
2526
import { SidebarNavigationHeader } from './SidebarNavigationHeader';
2627
import { SidebarNavigationItem } from './SidebarNavigationItem';
@@ -71,7 +72,31 @@ export const SidebarNavigation = Object.assign(SidebarNavigationRoot, {
7172
* </SidebarNavigation.Footer>
7273
* ```
7374
*/
74-
Footer: SidebarNavigationFooter,
75+
Footer: Object.assign(SidebarNavigationFooter, {
76+
/**
77+
* {@link SidebarNavigationFooterPromotionCard | PromotionCard} is a special PromotedSection-like component
78+
* specifically designed for the Sidebar's footer.
79+
*
80+
* It is a simplified version, and as such only has a few options
81+
*
82+
* ```tsx
83+
* <SidebarNavigation.Footer>
84+
* <SidebarNavigation.Footer.PromotionCard
85+
* actions={
86+
* <>
87+
* <Button>Try</Button>
88+
* <LinkStandalone to="/somewhere">No thanks</LinkStandalone>
89+
* </>
90+
* }
91+
* badgeText="..."
92+
* headerText="..."
93+
* text="..."
94+
* />
95+
* </SidebarNavigation.Footer>
96+
* ```
97+
*/
98+
PromotionCard: SidebarNavigationFooterPromotionCard,
99+
}),
75100

76101
/**
77102
* {@link SidebarNavigationGroup | Group} organizes navigation items under a common label,

0 commit comments

Comments
 (0)