Skip to content

Commit cfad056

Browse files
committed
[#11] Add BottomNav Component
Problem: Bottom navigation is one of the most important navigation ui elements nowadays. So we need to have it in our app also. Solution: `BottomNav` Component has been added + Tests + Animations.
1 parent 681d131 commit cfad056

File tree

7 files changed

+423
-3
lines changed

7 files changed

+423
-3
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@union-platform/ui",
33
"description": "UI-library of Union — a platform where you can find your labor of love, join a team, create something important or crazy; this is a place where you can be yourself.",
4-
"version": "0.1.37",
4+
"version": "0.1.38",
55
"private": false,
66
"main": "index.js",
77
"module": "index.mjs",
@@ -30,7 +30,8 @@
3030
"library"
3131
],
3232
"publishConfig": {
33-
"access": "public"
33+
"access": "public",
34+
"registry":"https://npm.pkg.github.com"
3435
},
3536
"dependencies": {
3637
"@radix-ui/react-accessible-icon": "^0.1.3",

src/BottomNav/BottomNav.stories.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// SPDX-FileCopyrightText: 2022 Union
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
import { ComponentStory, ComponentMeta } from '@storybook/react';
6+
import { useState } from 'react';
7+
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
8+
import ActivityIcon from '../Icons/ActivityIcon/ActivityIcon';
9+
import SearchIcon from '../Icons/SearchIcon/SearchIcon';
10+
import ProfileIcon from '../Icons/ProfileIcon/ProfileIcon';
11+
import MessagesIcon from '../Icons/MessagesIcon/MessagesIcon';
12+
13+
import BottomNav from './BottomNav';
14+
import BottomNavItem from './BottomNavItem/BottomNavItem';
15+
16+
export default {
17+
title: 'Union-UI/BottomNav',
18+
component: BottomNav,
19+
parameters: {
20+
layout: 'fullscreen',
21+
viewport: {
22+
viewports: INITIAL_VIEWPORTS,
23+
defaultViewport: 'iphone6',
24+
},
25+
design: {
26+
type: 'figma',
27+
url: 'https://www.figma.com/file/2St3zSul4fHnLffqy3WK7P/%5B-union-%5D-mobile?node-id=4840%3A37662',
28+
},
29+
},
30+
} as ComponentMeta<typeof BottomNav>;
31+
32+
const Template: ComponentStory<typeof BottomNav> = (args) => {
33+
const [isSelected, setIsSelected] = useState(0);
34+
35+
return (
36+
<BottomNav {...args}>
37+
<BottomNavItem
38+
borders="start"
39+
onClick={() => setIsSelected(0)}
40+
label="Search"
41+
isSelected={isSelected === 0}
42+
setIcon={({ fill, size, purposeLabel }) => (
43+
<SearchIcon
44+
purposeLabel={purposeLabel}
45+
fill={fill}
46+
size={size}
47+
/>
48+
)}
49+
/>
50+
<BottomNavItem
51+
borders="full"
52+
onClick={() => setIsSelected(1)}
53+
label="Messages"
54+
isSelected={isSelected === 1}
55+
setIcon={({ fill, size, purposeLabel }) => (
56+
<MessagesIcon
57+
purposeLabel={purposeLabel}
58+
fill={fill}
59+
size={size}
60+
/>
61+
)}
62+
/>
63+
<BottomNavItem
64+
borders="full"
65+
onClick={() => setIsSelected(2)}
66+
isSelected={isSelected === 2}
67+
setIcon={({ fill, size, purposeLabel }) => (
68+
<ProfileIcon
69+
purposeLabel={purposeLabel}
70+
fill={fill}
71+
size={size}
72+
/>
73+
)}
74+
/>
75+
<BottomNavItem
76+
borders="end"
77+
onClick={() => setIsSelected(3)}
78+
label="Activity"
79+
isSelected={isSelected === 3}
80+
setIcon={({ fill, size, purposeLabel }) => (
81+
<ActivityIcon
82+
purposeLabel={purposeLabel}
83+
fill={fill}
84+
size={size}
85+
/>
86+
)}
87+
/>
88+
</BottomNav>
89+
);
90+
};
91+
92+
export const Primary = Template.bind({});
93+
Primary.args = {
94+
95+
};

src/BottomNav/BottomNav.test.tsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// SPDX-FileCopyrightText: 2022 Union
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
import * as React from 'react';
6+
import { axe } from 'jest-axe';
7+
import { RenderResult, render, fireEvent } from '@testing-library/react';
8+
import { useEffect, useState } from 'react';
9+
import BottomNav from './BottomNav';
10+
import BottomNavItem from './BottomNavItem/BottomNavItem';
11+
import SearchIcon from '../Icons/SearchIcon/SearchIcon';
12+
import MessagesIcon from '../Icons/MessagesIcon/MessagesIcon';
13+
import ProfileIcon from '../Icons/ProfileIcon/ProfileIcon';
14+
import ActivityIcon from '../Icons/ActivityIcon/ActivityIcon';
15+
16+
const SECOND_ITEM_TEST_ID = 'bottom-nav-item-2';
17+
18+
global.ResizeObserver = class ResizeObserver {
19+
cb: any;
20+
21+
constructor(cb: any) {
22+
this.cb = cb;
23+
}
24+
25+
observe() {
26+
this.cb([{ borderBoxSize: { inlineSize: 0, blockSize: 0 } }]);
27+
}
28+
29+
unobserve() {}
30+
31+
disconnect() {}
32+
};
33+
34+
/* -------------------------------------------------------------------------------------------------
35+
* BottomNav
36+
* -----------------------------------------------------------------------------------------------*/
37+
38+
describe('given BottomNav with 4 BottomNavItems', () => {
39+
let rendered: RenderResult;
40+
let button: HTMLElement;
41+
let selectedItem = 0;
42+
43+
beforeEach(() => {
44+
rendered = render(<BottomNavTest cb={(v: number) => { selectedItem = v; }} />);
45+
button = rendered.getByTestId(SECOND_ITEM_TEST_ID);
46+
});
47+
48+
it('should have no accessibility violations', async () => {
49+
expect(await axe(rendered.container)).toHaveNoViolations();
50+
});
51+
52+
describe('when clicking the second BottomNavItem', () => {
53+
beforeEach(async () => {
54+
fireEvent.click(button);
55+
});
56+
57+
it('selected item should be equal to 1', () => {
58+
expect(selectedItem).toEqual(1);
59+
});
60+
});
61+
});
62+
63+
interface BottomNavTestProps {
64+
cb: (_arg: number) => void
65+
}
66+
67+
const BottomNavTest = ({ cb }: BottomNavTestProps) => {
68+
const [isSelected, setIsSelected] = useState(0);
69+
70+
useEffect(() => {
71+
cb(isSelected);
72+
}, [isSelected]);
73+
74+
return (
75+
<BottomNav>
76+
<BottomNavItem
77+
data-testid="bottom-nav-item-1"
78+
borders="start"
79+
onClick={() => setIsSelected(0)}
80+
label="Search"
81+
isSelected={isSelected === 0}
82+
setIcon={({ fill, size, purposeLabel }) => (
83+
<SearchIcon
84+
purposeLabel={purposeLabel}
85+
fill={fill}
86+
size={size}
87+
/>
88+
)}
89+
/>
90+
<BottomNavItem
91+
data-testid="bottom-nav-item-2"
92+
borders="full"
93+
onClick={() => setIsSelected(1)}
94+
label="Messages"
95+
isSelected={isSelected === 1}
96+
setIcon={({ fill, size, purposeLabel }) => (
97+
<MessagesIcon
98+
purposeLabel={purposeLabel}
99+
fill={fill}
100+
size={size}
101+
/>
102+
)}
103+
/>
104+
<BottomNavItem
105+
data-testid="bottom-nav-item-3"
106+
borders="full"
107+
onClick={() => setIsSelected(2)}
108+
label="Profile"
109+
isSelected={isSelected === 2}
110+
setIcon={({ fill, size, purposeLabel }) => (
111+
<ProfileIcon
112+
purposeLabel={purposeLabel}
113+
fill={fill}
114+
size={size}
115+
/>
116+
)}
117+
/>
118+
<BottomNavItem
119+
data-testid="bottom-nav-item-4"
120+
borders="end"
121+
onClick={() => setIsSelected(3)}
122+
label="Activity"
123+
isSelected={isSelected === 3}
124+
setIcon={({ fill, size, purposeLabel }) => (
125+
<ActivityIcon
126+
purposeLabel={purposeLabel}
127+
fill={fill}
128+
size={size}
129+
/>
130+
)}
131+
/>
132+
</BottomNav>
133+
);
134+
};

src/BottomNav/BottomNav.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// SPDX-FileCopyrightText: 2022 Union
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
import { styled } from '@stitches/react';
6+
import { AnimateSharedLayout } from 'framer-motion';
7+
import { ReactNode } from 'react';
8+
9+
export interface BottomNavProps {
10+
/**
11+
* Finite amount of `BottomNavItem`'s
12+
*/
13+
children: ReactNode[];
14+
}
15+
16+
const BottomNavContainer = styled('div', {
17+
width: '100%',
18+
margin: '8px 0 8px 0',
19+
display: 'flex',
20+
});
21+
22+
/**
23+
* Primary navigation component for mobile devices.
24+
*/
25+
const BottomNav = ({ children, ...props }: BottomNavProps) => (
26+
<AnimateSharedLayout>
27+
<BottomNavContainer {...props}>
28+
{children}
29+
</BottomNavContainer>
30+
</AnimateSharedLayout>
31+
);
32+
33+
export default BottomNav;

0 commit comments

Comments
 (0)