Skip to content

Commit 4154c46

Browse files
author
Juli Ovechkina
authored
Merge pull request #59 from gravity-ui/yuberdysheva/navigation
feat: add navigation
2 parents 6e45cef + da0304e commit 4154c46

File tree

34 files changed

+1523
-77
lines changed

34 files changed

+1523
-77
lines changed

README.md

Lines changed: 58 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,59 +32,81 @@ const Page: WithChildren<PageProps> = ({content}) => (
3232

3333
```typescript
3434
interface PageConstructorProps {
35-
content: PageContent; //Blocks data in JSON format.
36-
shouldRenderBlock?: ShouldRenderBlock; // A function that is invoked when rendering each block and lets you set conditions for its display.
37-
custom?: Custom; //Custom blocks (see `Customization`).
38-
renderMenu?: () => React.ReactNode; //A function that renders the page menu with navigation (we plan to add rendering for the default menu version).
35+
content: PageContent; //Blocks data in JSON format.
36+
shouldRenderBlock?: ShouldRenderBlock; // A function that is invoked when rendering each block and lets you set conditions for its display.
37+
custom?: Custom; //Custom blocks (see `Customization`).
38+
renderMenu?: () => React.ReactNode; //A function that renders the page menu with navigation (we plan to add rendering for the default menu version).
39+
navigation?: NavigationData; // Navigation data for using navigation component in JSON format
3940
}
4041

4142
interface PageConstructorProviderProps {
42-
isMobile?: boolean; //A flag indicating that the code is executed in mobile mode.
43-
locale?: LocaleContextProps; //Info about the language and domain (used when generating and formatting links).
44-
location?: Location; //API of the browser or router history, the page URL.
45-
metrika?: Metrika; //Functions for sending analytics
46-
ssrConfig?: SSR; //A flag indicating that the code is run on the server size.
47-
theme?: 'light' | 'dark'; //Theme to render the page with.
43+
isMobile?: boolean; //A flag indicating that the code is executed in mobile mode.
44+
locale?: LocaleContextProps; //Info about the language and domain (used when generating and formatting links).
45+
location?: Location; //API of the browser or router history, the page URL.
46+
metrika?: Metrika; //Functions for sending analytics
47+
ssrConfig?: SSR; //A flag indicating that the code is run on the server size.
48+
theme?: 'light' | 'dark'; //Theme to render the page with.
4849
}
4950

5051
export interface PageContent extends Animatable {
51-
blocks: Block[];
52-
menu?: Menu;
53-
background?: MediaProps;
54-
footnotes?: string[];
52+
blocks: Block[];
53+
menu?: Menu;
54+
background?: MediaProps;
55+
footnotes?: string[];
5556
}
5657

5758
interface Custom {
58-
blocks?: CustomItems;
59-
subBlocks?:CustomItems;
60-
headers?: CustomItems;
61-
loadable?: LoadableConfig;
59+
blocks?: CustomItems;
60+
subBlocks?: CustomItems;
61+
headers?: CustomItems;
62+
loadable?: LoadableConfig;
6263
}
6364

6465
type ShouldRenderBlock = (block: Block, blockKey: string) => Boolean;
6566

66-
interface Location = {
67-
history?: History;
68-
search?: string;
69-
hash?: string;
70-
pathname?: string;
71-
hostname?: string;
72-
};
73-
74-
interface Locale = {
75-
lang?: Lang;
76-
tld?: string;
77-
};
78-
79-
interface SSR = {
80-
isServer?: boolean;
67+
interface Location {
68+
history?: History;
69+
search?: string;
70+
hash?: string;
71+
pathname?: string;
72+
hostname?: string;
8173
}
8274

83-
interface Metrika = {
84-
metrika?: any;
85-
pixel?: any;
75+
interface Locale {
76+
lang?: Lang;
77+
tld?: string;
8678
}
8779

80+
interface SSR {
81+
isServer?: boolean;
82+
}
83+
84+
interface Metrika {
85+
metrika?: any;
86+
pixel?: any;
87+
}
88+
89+
interface NavigationData {
90+
logo: NavigationLogo;
91+
header: HeaderData;
92+
}
93+
94+
interface NavigationLogo {
95+
icon: ImageProps;
96+
text?: string;
97+
url?: string;
98+
}
99+
100+
interface HeaderData {
101+
leftItems: NavigationItem[];
102+
rightItems?: NavigationItem[];
103+
}
104+
105+
interface NavigationLogo {
106+
icon: ImageProps;
107+
text?: string;
108+
url?: string;
109+
}
88110
```
89111

90112
### Custom blocks

src/components/OverflowScroller/OverflowScroller.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ $block: '.#{$ns}overflow-scroller';
1616
&__scroller {
1717
position: absolute;
1818
z-index: 10;
19+
top: 0;
1920

2021
display: flex;
2122
justify-content: flex-end;
2223
align-items: center;
2324

2425
width: 32px;
26+
height: calc(100% - 1px);
2527

2628
cursor: pointer;
2729

src/components/RouterLink/RouterLink.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {WithChildren} from '../../models';
44

55
export interface RouterLinkProps {
66
href: string;
7+
[key: string]: unknown;
78
}
89

910
const RouterLink = ({href, children}: WithChildren<RouterLinkProps>) => {

src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,7 @@ export {default as OverflowScroller} from './OverflowScroller/OverflowScroller';
3333
export {default as Author} from './Author/Author';
3434
export {default as RouterLink} from './RouterLink/RouterLink';
3535
export {default as HTML} from './HTML/HTML';
36+
export {default as Header} from './navigation/components/Header/Header';
37+
export * as Navigation from './navigation/components/index';
3638

3739
export type {RouterLinkProps} from './RouterLink/RouterLink';
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
@import '../../../../../styles/variables.scss';
2+
@import '../../../../../styles/mixins.scss';
3+
4+
$block: '.#{$ns}header';
5+
6+
#{$block} {
7+
$root: &;
8+
9+
position: sticky;
10+
z-index: 98;
11+
top: 0;
12+
13+
display: flex;
14+
justify-content: center;
15+
align-items: center;
16+
17+
height: var(--header-height);
18+
19+
background-color: var(--yc-color-base-background);
20+
box-shadow: inset 0 -1px 0 var(--yc-color-line-generic);
21+
22+
&__wrapper {
23+
display: flex;
24+
justify-content: space-between;
25+
align-items: center;
26+
27+
height: var(--header-height);
28+
}
29+
30+
%menu-button {
31+
position: absolute;
32+
z-index: 2;
33+
}
34+
35+
&__mobile-menu-button {
36+
@include mobile-tablet-only();
37+
}
38+
39+
&__navigation,
40+
&__left,
41+
&__right {
42+
display: flex;
43+
align-items: center;
44+
}
45+
46+
&__navigation {
47+
position: relative;
48+
margin-right: $normalOffset;
49+
50+
flex: 1 0 0;
51+
justify-content: flex-start;
52+
53+
@include desktop-only();
54+
}
55+
56+
&__right {
57+
flex: 0;
58+
justify-content: flex-end;
59+
}
60+
61+
&__navigation-container {
62+
display: flex;
63+
overflow-x: hidden;
64+
flex: 1 0 0;
65+
justify-content: space-between;
66+
align-items: center;
67+
68+
margin-right: $indentS;
69+
}
70+
71+
&__buttons {
72+
display: flex;
73+
@include desktop-only();
74+
75+
& > * {
76+
&:not(:last-child) {
77+
margin-right: $indentXS;
78+
}
79+
}
80+
}
81+
82+
&__button {
83+
margin-top: 0;
84+
}
85+
86+
&__logo {
87+
margin: 0 $indentM 0 0;
88+
89+
cursor: pointer;
90+
}
91+
92+
@media (max-width: map-get($gridBreakpoints, 'md') - 1) {
93+
&__navigation-container {
94+
justify-content: flex-end;
95+
}
96+
97+
&__left {
98+
flex: 1 0 0;
99+
}
100+
}
101+
102+
@media (max-width: map-get($gridBreakpoints, 'sm') - 1) {
103+
&__navigation-container {
104+
margin-right: $indentXXS;
105+
}
106+
107+
&__logo {
108+
margin-right: 0;
109+
}
110+
}
111+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, {MouseEvent, useCallback, useState} from 'react';
2+
3+
import {block} from '../../../../utils';
4+
import {HeaderData, NavigationLogo} from '../../../../models';
5+
import {Col, Grid, Row} from '../../../../grid';
6+
import OutsideClick from '../../../OutsideClick/OutsideClick';
7+
import Control from '../../../Control/Control';
8+
import Navigation from '../Navigation/Navigation';
9+
import MobileNavigation from '../MobileNavigation/MobileNavigation';
10+
import NavigationItem from '../NavigationItem/NavigationItem';
11+
import Logo from '../Logo/Logo';
12+
13+
import {NavigationClose, NavigationOpen} from '../../../../icons';
14+
15+
import './Header.scss';
16+
17+
const b = block('header');
18+
19+
const ICON_SIZE = 36;
20+
21+
export interface HeaderProps {
22+
logo: NavigationLogo;
23+
data: HeaderData;
24+
}
25+
26+
interface MobileMenuButtonProps {
27+
isSidebarOpened: boolean;
28+
onSidebarOpenedChange: (arg: boolean) => void;
29+
}
30+
31+
const MobileMenuButton: React.FC<MobileMenuButtonProps> = ({
32+
isSidebarOpened,
33+
onSidebarOpenedChange,
34+
}) => {
35+
const iconProps = {
36+
icon: isSidebarOpened ? NavigationClose : NavigationOpen,
37+
iconSize: ICON_SIZE,
38+
};
39+
40+
return (
41+
<Control
42+
className={b('mobile-menu-button')}
43+
onClick={(e: MouseEvent) => {
44+
e.stopPropagation();
45+
onSidebarOpenedChange(!isSidebarOpened);
46+
}}
47+
size="l"
48+
{...iconProps}
49+
/>
50+
);
51+
};
52+
53+
export const Header: React.FC<HeaderProps> = ({data, logo}) => {
54+
const {leftItems, rightItems} = data;
55+
const [isSidebarOpened, setIsSidebarOpened] = useState(false);
56+
const [activeItemIndex, setActiveItemIndex] = useState(-1);
57+
58+
const onActiveItemChange = useCallback((index: number) => {
59+
setActiveItemIndex(index);
60+
}, []);
61+
62+
const onSidebarOpenedChange = useCallback((isOpen: boolean) => {
63+
setIsSidebarOpened(isOpen);
64+
}, []);
65+
66+
const hideSidebar = useCallback(() => {
67+
setIsSidebarOpened(false);
68+
}, []);
69+
70+
return (
71+
<Grid className={b()}>
72+
<Row>
73+
<Col>
74+
<header className={b('wrapper')}>
75+
{logo && (
76+
<div className={b('left')}>
77+
<Logo {...logo} className={b('logo')} />
78+
</div>
79+
)}
80+
<div className={b('navigation-container')}>
81+
<Navigation
82+
className={b('navigation')}
83+
links={leftItems}
84+
activeItemIndex={activeItemIndex}
85+
onActiveItemChange={onActiveItemChange}
86+
/>
87+
</div>
88+
<div className={b('right')}>
89+
<MobileMenuButton
90+
isSidebarOpened={isSidebarOpened}
91+
onSidebarOpenedChange={onSidebarOpenedChange}
92+
/>
93+
{rightItems && (
94+
<div className={b('buttons')}>
95+
{rightItems.map((button) => (
96+
<NavigationItem
97+
key={button.text}
98+
data={button}
99+
className={b('button')}
100+
/>
101+
))}
102+
</div>
103+
)}
104+
</div>
105+
<OutsideClick onOutsideClick={() => onSidebarOpenedChange(false)}>
106+
<MobileNavigation
107+
topItems={leftItems}
108+
bottomItems={rightItems}
109+
isOpened={isSidebarOpened}
110+
activeItemIndex={activeItemIndex}
111+
onActiveItemChange={onActiveItemChange}
112+
onClose={hideSidebar}
113+
/>
114+
</OutsideClick>
115+
</header>
116+
</Col>
117+
</Row>
118+
</Grid>
119+
);
120+
};
121+
122+
export default Header;

0 commit comments

Comments
 (0)