Skip to content

Commit 6cd9289

Browse files
JeikZimRuslan Bagautdinov
and
Ruslan Bagautdinov
authored
feat: add mobile header items and renderNavigation method (#989)
* refactor: component Control using Button from uikit * refactor: add exports in navigation * feat: add mobile header items * refactor: navigation component Navigation * refactor: replace ControlProps.size with type on ButtonSize from uikit * revert: component MobileMenuButton size * revert: menuLayout mobileHorizontal * refactor: rename mobileHeaderItems to customMobileHeaderItems * Revert "refactor: component Control using Button from uikit" This reverts commit 82b85ab. * refactor: add JSDoc description for customMobileHeaderItems props * revert: change size in MobileMenuButton * refactor: move the custom hooks to a separate files * refactor: replace hooks into navgiation folder * fix: mobile header flex model * feat: add story in Navigation.story.book.tsx * fix: change customMobileHeader items storie on one icon in list * fix: type error * feat: add padding to mobile header items --------- Co-authored-by: Ruslan Bagautdinov <[email protected]>
1 parent 65b89c5 commit 6cd9289

File tree

18 files changed

+231
-55
lines changed

18 files changed

+231
-55
lines changed

src/models/navigation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ export type ThemedNavigationLogoData = NavigationLogoData & ThemeSupporting<Navi
9191
export interface HeaderData {
9292
leftItems: NavigationItemModel[];
9393
rightItems?: NavigationItemModel[];
94+
95+
/**
96+
* Items for the navigation header on mobile devices.
97+
* They are located to the right of the Logo and to the left of the MobileMenuButton.
98+
* @type {NavigationItemModel[]}
99+
*/
100+
customMobileHeaderItems?: NavigationItemModel[];
94101
iconSize?: number;
95102
withBorder?: boolean;
96103
withBorderOnScroll?: boolean;
@@ -116,4 +123,5 @@ export interface NavigationData {
116123
logo: ThemedNavigationLogoData;
117124
header: HeaderData;
118125
footer?: FooterData;
126+
renderNavigation?: () => React.ReactNode;
119127
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.custom-button {
2+
white-space: nowrap;
3+
4+
transition: transform 300ms;
5+
6+
&_active {
7+
transform: scale(0.9);
8+
}
9+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
3+
import {TriangleUp} from '@gravity-ui/icons';
4+
import {Button} from '@gravity-ui/uikit';
5+
6+
import {cn} from '../../../utils';
7+
import {NavigationItemProps} from '../../models';
8+
9+
import './CustomButton.scss';
10+
11+
const b = cn('custom-button');
12+
13+
type DCDropdownNavigationItemProps = Pick<NavigationItemProps, 'onClick' | 'isActive'>;
14+
15+
export const CustomButton: React.FC<DCDropdownNavigationItemProps> = (props) => {
16+
const {onClick, isActive} = props;
17+
18+
return (
19+
<Button size="l" view="flat" className={b({active: isActive})} onClick={onClick}>
20+
<Button.Icon>
21+
<TriangleUp height={20} width={20} />
22+
</Button.Icon>
23+
</Button>
24+
);
25+
};

src/navigation/__stories__/Navigation.stories.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {Meta, StoryFn} from '@storybook/react';
55
import {PageConstructor} from '../../containers/PageConstructor';
66
import {CustomConfig, NavigationData} from '../../models';
77

8+
import {CustomButton} from './CustomButton/CustomButton';
89
import {CustomComponent} from './CustomComponent/CustomComponent';
910

1011
import data from './data.json';
@@ -21,6 +22,7 @@ const DefaultTemplate: StoryFn<{
2122
export const DefaultNavigation = DefaultTemplate.bind({});
2223
export const NavigationWithBorder = DefaultTemplate.bind({});
2324
export const NavigationWithCustomItems = DefaultTemplate.bind({});
25+
export const NavigationWithCustomMobileHeaderItems = DefaultTemplate.bind({});
2426

2527
DefaultNavigation.args = {
2628
navigation: data.navigation as NavigationData,
@@ -56,3 +58,18 @@ NavigationWithCustomItems.args = {
5658
},
5759
} as NavigationData,
5860
};
61+
62+
NavigationWithCustomMobileHeaderItems.args = {
63+
custom: {
64+
navigation: {
65+
'custom-item': CustomButton,
66+
},
67+
},
68+
navigation: {
69+
...data.navigation,
70+
header: {
71+
...data.navigation.header,
72+
customMobileHeaderItems: [{type: 'custom-item'}],
73+
},
74+
} as unknown as NavigationData,
75+
};

src/navigation/components/DesktopNavigation/DesktopNavigation.scss

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,47 @@ $block: '.#{$ns}desktop-navigation';
4141
}
4242
}
4343

44+
&__mobile-navigation {
45+
@include add-specificity(&) {
46+
justify-content: flex-end;
47+
48+
@include mobile-tablet-only();
49+
}
50+
51+
&-container {
52+
padding-right: $indentXXXS;
53+
}
54+
55+
&-container:has(.#{$ns}overflow-scroller__arrow) & {
56+
justify-content: flex-start;
57+
}
58+
59+
&-container:has(.#{$ns}overflow-scroller__arrow_type_right) {
60+
padding-right: $indentXS;
61+
}
62+
}
63+
4464
&__right {
45-
flex: 0;
4665
justify-content: flex-end;
4766

4867
@include text-size(body-2);
68+
69+
@media (max-width: map-get($gridBreakpoints,'md') - 1) {
70+
flex: 3 0 0;
71+
max-width: 50%;
72+
}
4973
}
5074

51-
&__navigation-container {
75+
&__navigation-container,
76+
&__mobile-navigation-container {
5277
display: flex;
5378
overflow-x: hidden;
5479
flex: 1 0 0;
5580
justify-content: space-between;
5681
align-items: center;
82+
}
5783

84+
&__navigation-container {
5885
margin-right: $indentM;
5986
}
6087

@@ -68,8 +95,9 @@ $block: '.#{$ns}desktop-navigation';
6895
cursor: pointer;
6996
}
7097

98+
&__links,
7199
&__buttons,
72-
&__links {
100+
&__mobile-buttons {
73101
display: flex;
74102
align-items: center;
75103

@@ -86,6 +114,16 @@ $block: '.#{$ns}desktop-navigation';
86114
}
87115
}
88116

117+
&__mobile-buttons {
118+
@include mobile-tablet-only();
119+
}
120+
121+
&__mobile-buttons &__item {
122+
&:not(:last-child) {
123+
margin-right: 0;
124+
}
125+
}
126+
89127
&__links {
90128
position: relative;
91129

@@ -105,6 +143,15 @@ $block: '.#{$ns}desktop-navigation';
105143
justify-content: flex-end;
106144
}
107145

146+
&__navigation-container,
147+
&__mobile-navigation-container {
148+
justify-content: flex-end;
149+
}
150+
151+
&__navigation-container {
152+
flex: 0 0 0;
153+
}
154+
108155
&__left {
109156
flex: 1 0 0;
110157
}

src/navigation/components/DesktopNavigation/DesktopNavigation.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import './DesktopNavigation.scss';
1111

1212
const b = block('desktop-navigation');
1313

14-
const DesktopNavigation: React.FC<DesktopNavigationProps> = ({
14+
export const DesktopNavigation: React.FC<DesktopNavigationProps> = ({
1515
logo,
1616
leftItemsWithIconSize,
1717
rightItemsWithIconSize,
18+
customMobileHeaderItems,
1819
isSidebarOpened,
1920
onSidebarOpenedChange,
2021
onActiveItemChange,
@@ -40,6 +41,25 @@ const DesktopNavigation: React.FC<DesktopNavigationProps> = ({
4041
</OverflowScroller>
4142
</div>
4243
<div className={b('right')}>
44+
{customMobileHeaderItems && (
45+
<div className={b('mobile-navigation-container')}>
46+
<OverflowScroller
47+
className={b('mobile-navigation')}
48+
onScrollStart={onActiveItemChange}
49+
arrowSize={18}
50+
>
51+
<NavigationList
52+
items={customMobileHeaderItems}
53+
onActiveItemChange={onActiveItemChange}
54+
className={b('mobile-buttons')}
55+
itemClassName={b('item')}
56+
column={ItemColumnName.Left}
57+
activeItemId={activeItemId}
58+
menuLayout={NavigationLayout.Dropdown}
59+
/>
60+
</OverflowScroller>
61+
</div>
62+
)}
4363
<MobileMenuButton
4464
isSidebarOpened={isSidebarOpened}
4565
onSidebarOpenedChange={onSidebarOpenedChange}

src/navigation/components/Logo/Logo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type LogoProps = ThemedNavigationLogoData & {
1919
alt?: string;
2020
};
2121

22-
const Logo: React.FC<LogoProps> = ({alt = i18n('image-alt'), ...restProps}) => {
22+
export const Logo: React.FC<LogoProps> = ({alt = i18n('image-alt'), ...restProps}) => {
2323
const props: LogoProps = {...restProps, alt};
2424
const {hostname, Link} = useContext(LocationContext);
2525
const theme = useTheme();

src/navigation/components/MobileNavigation/MobileNavigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import './MobileNavigation.scss';
1212

1313
const b = block('mobile-navigation');
1414

15-
const MobileNavigation: React.FC<MobileNavigationProps> = ({
15+
export const MobileNavigation: React.FC<MobileNavigationProps> = ({
1616
isOpened,
1717
topItems,
1818
bottomItems,

src/navigation/components/Navigation/Navigation.tsx

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,39 @@
1-
import React, {useEffect, useMemo, useState} from 'react';
2-
3-
import debounce from 'lodash/debounce';
1+
import React, {useState} from 'react';
42

53
import OutsideClick from '../../../components/OutsideClick/OutsideClick';
64
import {Col, Grid, Row} from '../../../grid';
75
import {ClassNameProps, HeaderData, ThemedNavigationLogoData} from '../../../models';
86
import {block} from '../../../utils';
9-
import {getNavigationItemWithIconSize} from '../../utils';
7+
import {useActiveNavItem, useShowBorder} from '../../hooks';
108
import DesktopNavigation from '../DesktopNavigation/DesktopNavigation';
119
import MobileNavigation from '../MobileNavigation/MobileNavigation';
1210

1311
import './Navigation.scss';
1412

1513
const b = block('navigation');
1614

17-
export interface NavigationProps extends ClassNameProps {
15+
export interface NavigationComponentProps extends ClassNameProps {
1816
logo: ThemedNavigationLogoData;
1917
data: HeaderData;
2018
}
2119

22-
export const Navigation: React.FC<NavigationProps> = ({data, logo, className}) => {
20+
export const Navigation: React.FC<NavigationComponentProps> = ({data, logo, className}) => {
2321
const {
2422
leftItems,
2523
rightItems,
24+
customMobileHeaderItems,
2625
iconSize = 20,
2726
withBorder = false,
2827
withBorderOnScroll = true,
2928
} = data;
30-
const [isSidebarOpened, setIsSidebarOpened] = useState(false);
31-
const [activeItemId, setActiveItemId] = useState<string | undefined>(undefined);
32-
const [showBorder, setShowBorder] = useState(withBorder);
33-
34-
const getNavigationItem = getNavigationItemWithIconSize(iconSize);
35-
36-
const leftItemsWithIconSize = useMemo(
37-
() => leftItems.map(getNavigationItem),
38-
[getNavigationItem, leftItems],
39-
);
40-
const rightItemsWithIconSize = useMemo(
41-
() => rightItems?.map(getNavigationItem),
42-
[getNavigationItem, rightItems],
43-
);
4429

45-
const onActiveItemChange = (id?: string) => {
46-
setActiveItemId(id);
47-
};
30+
const [isSidebarOpened, setIsSidebarOpened] = useState(false);
31+
const [showBorder] = useShowBorder(withBorder, withBorderOnScroll);
32+
const {activeItemId, leftItemsWithIconSize, rightItemsWithIconSize, onActiveItemChange} =
33+
useActiveNavItem(iconSize, leftItems, rightItems);
4834

4935
const onSidebarOpenedChange = (isOpen: boolean) => setIsSidebarOpened(isOpen);
5036

51-
useEffect(() => {
52-
if (!withBorderOnScroll) return () => {};
53-
54-
const showBorderOnScroll = () => {
55-
if (!withBorder) {
56-
setShowBorder(window.scrollY > 0);
57-
}
58-
};
59-
60-
const scrollHandler = debounce(showBorderOnScroll, 20);
61-
62-
window.addEventListener('scroll', scrollHandler, {passive: true});
63-
return () => window.removeEventListener('scroll', scrollHandler);
64-
});
65-
6637
return (
6738
<Grid className={b({'with-border': showBorder}, className)}>
6839
<Row>
@@ -74,6 +45,7 @@ export const Navigation: React.FC<NavigationProps> = ({data, logo, className}) =
7445
onActiveItemChange={onActiveItemChange}
7546
leftItemsWithIconSize={leftItemsWithIconSize}
7647
rightItemsWithIconSize={rightItemsWithIconSize}
48+
customMobileHeaderItems={customMobileHeaderItems}
7749
isSidebarOpened={isSidebarOpened}
7850
onSidebarOpenedChange={onSidebarOpenedChange}
7951
/>

src/navigation/components/NavigationItem/NavigationItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const nonComplexNavigationItemTypes = NavigationItemTypes.filter(
1919
(type) => type !== NavigationItemType.Dropdown,
2020
);
2121

22-
const NavigationItem: React.FC<NavigationItemProps> = ({
22+
export const NavigationItem: React.FC<NavigationItemProps> = ({
2323
data,
2424
className,
2525
menuLayout,

src/navigation/components/NavigationListItem/NavigationListItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {NavigationListItemProps} from '../../models';
44
import {getItemClickHandler} from '../../utils';
55
import NavigationItem from '../NavigationItem';
66

7-
const NavigationListItem: React.FC<NavigationListItemProps> = ({
7+
export const NavigationListItem: React.FC<NavigationListItemProps> = ({
88
column,
99
index,
1010
activeItemId,

src/navigation/components/Standalone/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import React from 'react';
22

33
import RootCn from '../../../components/RootCn';
44

5-
import Navigation, {NavigationProps} from './../../components/Navigation/Navigation';
5+
import Navigation, {NavigationComponentProps} from './../../components/Navigation/Navigation';
66

7-
const Standalone = (props: NavigationProps) => (
7+
const Standalone = (props: NavigationComponentProps) => (
88
<RootCn>
99
<Navigation {...props} />
1010
</RootCn>

src/navigation/containers/Layout/Layout.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ export interface LayoutProps {
1515

1616
const Layout: React.FC<LayoutProps> = ({children, navigation}) => (
1717
<div className={b()}>
18-
{navigation && (
19-
<Navigation
20-
data={navigation.header}
21-
logo={navigation.logo}
22-
className={b('navigation')}
23-
/>
24-
)}
18+
{navigation &&
19+
(navigation.renderNavigation ? (
20+
navigation.renderNavigation()
21+
) : (
22+
<Navigation
23+
data={navigation.header}
24+
logo={navigation.logo}
25+
className={b('navigation')}
26+
/>
27+
))}
2528
<main className={b('content')}>{children}</main>
2629
</div>
2730
);

src/navigation/hooks/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export {default as useActiveNavItem} from './useActiveNavItem';
2+
export {default as useShowBorder} from './useShowBorder';

0 commit comments

Comments
 (0)