Skip to content

Commit abc410f

Browse files
authored
Merge pull request #781 from glints-dev/feature/next-bar
add bar component
2 parents b53df25 + dfe23ca commit abc410f

13 files changed

Lines changed: 357 additions & 0 deletions

src/@next/Bar/Bar.stories.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React from 'react';
2+
import { Story, Meta } from '@storybook/react';
3+
4+
import { BaseContainer } from '../../Layout/GlintsContainer/GlintsContainer';
5+
import { Bar, BarProps } from './Bar';
6+
import { Colors, Typography } from '..';
7+
import { StyledCustomHeadingWrapper } from './BarStyle';
8+
9+
(Bar as React.FunctionComponent<BarProps>).displayName = 'Bar';
10+
11+
export default {
12+
title: '@next/Bar',
13+
component: Bar,
14+
decorators: [
15+
Story => (
16+
<BaseContainer style={{ height: '200px' }}>{Story()}</BaseContainer>
17+
),
18+
],
19+
} as Meta;
20+
21+
const Template: Story<BarProps> = args => {
22+
const primaryAction = {
23+
label: 'Yes',
24+
action: () => console.log('Primary action!'),
25+
};
26+
const secondaryAction = {
27+
label: 'No',
28+
action: () => console.log('Secondary action!'),
29+
};
30+
const tertiaryAction = {
31+
label: 'No',
32+
action: () => console.log('Tertiary action!'),
33+
};
34+
return (
35+
<Bar
36+
{...args}
37+
primaryAction={primaryAction}
38+
secondaryAction={secondaryAction}
39+
tertiaryAction={tertiaryAction}
40+
/>
41+
);
42+
};
43+
44+
export const Interactive = Template.bind({});
45+
Interactive.args = {
46+
heading: 'Heading',
47+
subheading: 'SubHeading',
48+
position: 'top',
49+
};
50+
Interactive.parameters = {
51+
docs: {
52+
source: {
53+
code: `
54+
<Bar
55+
heading="Heading"
56+
primaryAction={{
57+
action: () => {console.log('Primary action!')},
58+
label: 'Yes'
59+
}}
60+
secondaryAction={{
61+
action: () => {console.log('Secondary action!')},
62+
label: 'No'
63+
}}
64+
subheading="Subheading"
65+
tertiaryAction={{
66+
action: () => {console.log('Tertiary action!')},
67+
label: 'No'
68+
}}
69+
/>
70+
`,
71+
},
72+
language: 'javascript',
73+
type: 'auto',
74+
},
75+
};
76+
77+
const PrimaryActionOnlyTemplate: Story<BarProps> = args => {
78+
const primaryAction = {
79+
label: 'Yes',
80+
action: () => console.log('Primary action!'),
81+
};
82+
return <Bar {...args} primaryAction={primaryAction} />;
83+
};
84+
85+
export const PrimaryActionOnly = PrimaryActionOnlyTemplate.bind({});
86+
PrimaryActionOnly.args = {
87+
heading: 'Heading',
88+
};
89+
90+
const CustomHeadingTemplate: Story<BarProps> = args => {
91+
const headingMarkup = (
92+
<StyledCustomHeadingWrapper>
93+
<Typography as="span">Status:</Typography>
94+
<Typography as="span" color={Colors.Orange.S86}>
95+
DRAFT
96+
</Typography>
97+
</StyledCustomHeadingWrapper>
98+
);
99+
const primaryAction = {
100+
label: 'Yes',
101+
action: () => console.log('Yes'),
102+
};
103+
const secondaryAction = {
104+
label: 'No',
105+
action: () => console.log('No'),
106+
};
107+
108+
return (
109+
<Bar
110+
{...args}
111+
heading={headingMarkup}
112+
subheading={'Autosaved to draft'}
113+
primaryAction={primaryAction}
114+
secondaryAction={secondaryAction}
115+
/>
116+
);
117+
};
118+
119+
export const CustomHeading = CustomHeadingTemplate.bind({});

src/@next/Bar/Bar.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
import { Breakpoints, Colors } from '..';
3+
import { ComponentAction } from '../../types/componentAction';
4+
import { Button, PrimaryButton } from '../Button';
5+
import { ButtonGroup } from '../ButtonGroup';
6+
import { Typography } from '../Typography';
7+
import {
8+
StyledBarActionWrapper,
9+
StyledBar,
10+
StyledBarHeaderWrapper,
11+
StyledBarContainer,
12+
} from './BarStyle';
13+
import { useWindowSize } from './useWindowSize';
14+
15+
export type BarProps = {
16+
heading: React.ReactNode;
17+
subheading?: React.ReactNode;
18+
primaryAction: ComponentAction;
19+
secondaryAction?: ComponentAction;
20+
tertiaryAction?: ComponentAction;
21+
position?: 'top' | 'bottom';
22+
};
23+
24+
export const Bar = React.forwardRef<HTMLDivElement, BarProps>(function Bar(
25+
{
26+
heading,
27+
subheading,
28+
primaryAction,
29+
secondaryAction,
30+
tertiaryAction,
31+
position,
32+
}: BarProps,
33+
ref
34+
) {
35+
const headerMarkup = () => {
36+
if (subheading) {
37+
return (
38+
<>
39+
<Typography as="div" variant="body2" color={Colors.Neutral.B18}>
40+
{heading}
41+
</Typography>
42+
<Typography as="div" variant="subtitle2" color={Colors.Neutral.B40}>
43+
{subheading}
44+
</Typography>
45+
</>
46+
);
47+
}
48+
return (
49+
<Typography as="div" variant="headline6" color={Colors.Neutral.B18}>
50+
{heading}
51+
</Typography>
52+
);
53+
};
54+
const { width } = useWindowSize();
55+
const breakpointWidth = parseInt(Breakpoints.large.slice(0, -2));
56+
const buttonSize: 'default' | 'large' =
57+
width <= breakpointWidth ? 'default' : 'large';
58+
59+
return (
60+
<StyledBar data-align={position} ref={ref}>
61+
<StyledBarContainer>
62+
<StyledBarHeaderWrapper>{headerMarkup()} </StyledBarHeaderWrapper>
63+
<StyledBarActionWrapper>
64+
<ButtonGroup>
65+
{tertiaryAction && (
66+
<Button onClick={tertiaryAction.action} size={buttonSize}>
67+
{tertiaryAction.label}
68+
</Button>
69+
)}
70+
{secondaryAction && (
71+
<Button onClick={secondaryAction.action} size={buttonSize}>
72+
{secondaryAction.label}
73+
</Button>
74+
)}
75+
<PrimaryButton onClick={primaryAction.action} size={buttonSize}>
76+
{primaryAction.label}
77+
</PrimaryButton>
78+
</ButtonGroup>
79+
</StyledBarActionWrapper>
80+
</StyledBarContainer>
81+
</StyledBar>
82+
);
83+
});

src/@next/Bar/BarStyle.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import styled from 'styled-components';
2+
import * as Breakpoints from '../utilities/breakpoints';
3+
import { Neutral } from '../utilities/colors';
4+
import { space8 } from '../utilities/spacing';
5+
6+
export const StyledBar = styled.div`
7+
display: flex;
8+
flex-direction: row;
9+
padding: 20px 0;
10+
width: 100%;
11+
position: absolute;
12+
height: 90px;
13+
14+
@media (max-width: ${Breakpoints.large}) {
15+
height: 70px;
16+
}
17+
18+
&[data-align='bottom'] {
19+
bottom: 0;
20+
}
21+
&[data-align='top'] {
22+
top: 0;
23+
}
24+
25+
box-shadow: 0px 0px 0px 1px rgba(63, 63, 68, 0.05),
26+
0px 1px 3px rgba(63, 63, 68, 0.15);
27+
background: ${Neutral.B100};
28+
`;
29+
30+
export const StyledBarHeaderWrapper = styled.div`
31+
display: flex;
32+
flex-grow: 1;
33+
flex-direction: column;
34+
`;
35+
36+
export const StyledBarActionWrapper = styled.div`
37+
display: flex;
38+
flex-direction: column-reverse;
39+
align-items: flex-end;
40+
`;
41+
42+
export const StyledBarContainer = styled.div`
43+
position: relative;
44+
margin: 0 auto;
45+
padding: 0 50px;
46+
width: 100%;
47+
48+
*,
49+
*:before,
50+
*:after {
51+
box-sizing: border-box;
52+
}
53+
54+
justify-content: space-between;
55+
align-items: center;
56+
flex-direction: row;
57+
display: flex;
58+
59+
@media (max-width: ${Breakpoints.large}) {
60+
gap: 16px;
61+
padding: 0 24px;
62+
}
63+
`;
64+
65+
export const StyledCustomHeadingWrapper = styled.div`
66+
display: flex;
67+
flex-direction: row;
68+
gap: ${space8};
69+
`;

src/@next/Bar/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Bar';

src/@next/Bar/useWindowSize.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useState, useEffect } from 'react';
2+
3+
// Hook
4+
export const useWindowSize = () => {
5+
// Initialize state with undefined width/height so server and client renders match
6+
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
7+
const [windowSize, setWindowSize] = useState({
8+
width: undefined,
9+
height: undefined,
10+
});
11+
12+
useEffect(() => {
13+
// Handler to call on window resize
14+
function handleResize() {
15+
// Set window width/height to state
16+
setWindowSize({
17+
width: window.innerWidth,
18+
height: window.innerHeight,
19+
});
20+
}
21+
22+
// Add event listener
23+
window.addEventListener('resize', handleResize);
24+
25+
// Call handler right away so state gets updated with initial window size
26+
handleResize();
27+
28+
// Remove event listener on cleanup
29+
return () => window.removeEventListener('resize', handleResize);
30+
}, []); // Empty array ensures that effect is only run on mount
31+
32+
return windowSize;
33+
};
34+
35+
// Taken from https://usehooks.com/useWindowSize/

src/@next/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export {
2424
export { Avatar, AvatarProps } from './Avatar';
2525
export { Badge, BadgeProps } from './Badge';
2626
export { Banner, BannerProps } from './Banner';
27+
export { Bar, BarProps } from './Bar';
2728
export {
2829
Button,
2930
ButtonProps,

test/e2e/bar/bar.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { test, expect, Page } from '@playwright/test';
2+
import { StoryBookPage } from '../storybookPage';
3+
4+
const getPage = (page: Page) =>
5+
new StoryBookPage(page, '?path=/story/next-bar--interactive');
6+
7+
test('Bar', async ({ page }) => {
8+
const barPage = getPage(page);
9+
await barPage.goto();
10+
await expect(barPage.canvas).toHaveScreenshot('bar.png');
11+
});
12+
13+
test('Bar - without subheading', async ({ page }) => {
14+
const barPage = getPage(page);
15+
await barPage.goto('args=subheading:!null');
16+
await expect(barPage.canvas).toHaveScreenshot('bar-no-subheading.png');
17+
});
18+
19+
test('Bar - position top', async ({ page }) => {
20+
const barPage = getPage(page);
21+
await barPage.goto('args=position:top');
22+
await expect(barPage.canvas).toHaveScreenshot('bar-actions-position-top.png');
23+
});
24+
25+
test('Bar - position bottom', async ({ page }) => {
26+
const barPage = getPage(page);
27+
await barPage.goto('args=position:bottom');
28+
await expect(barPage.canvas).toHaveScreenshot(
29+
'bar-actions-position-bottom.png'
30+
);
31+
});
32+
33+
test('Bar - primary action only', async ({ page }) => {
34+
const barPage = new StoryBookPage(
35+
page,
36+
'?path=/story/next-bar--primary-action-only'
37+
);
38+
await barPage.goto();
39+
await expect(barPage.canvas).toHaveScreenshot('bar-primary-action-only.png');
40+
});
41+
42+
test('Bar - Custom Heading and Sub Heading', async ({ page }) => {
43+
const barPage = new StoryBookPage(
44+
page,
45+
'?path=/story/next-bar--custom-heading'
46+
);
47+
await barPage.goto();
48+
await expect(barPage.canvas).toHaveScreenshot('bar-custom-heading.png');
49+
});
9.43 KB
Loading
9.49 KB
Loading
9.49 KB
Loading

0 commit comments

Comments
 (0)