Skip to content

Commit

Permalink
feat: add mobile header items and renderNavigation method (#989)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
JeikZim and Ruslan Bagautdinov authored Aug 30, 2024
1 parent 65b89c5 commit 6cd9289
Show file tree
Hide file tree
Showing 18 changed files with 231 additions and 55 deletions.
8 changes: 8 additions & 0 deletions src/models/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export type ThemedNavigationLogoData = NavigationLogoData & ThemeSupporting<Navi
export interface HeaderData {
leftItems: NavigationItemModel[];
rightItems?: NavigationItemModel[];

/**
* Items for the navigation header on mobile devices.
* They are located to the right of the Logo and to the left of the MobileMenuButton.
* @type {NavigationItemModel[]}
*/
customMobileHeaderItems?: NavigationItemModel[];
iconSize?: number;
withBorder?: boolean;
withBorderOnScroll?: boolean;
Expand All @@ -116,4 +123,5 @@ export interface NavigationData {
logo: ThemedNavigationLogoData;
header: HeaderData;
footer?: FooterData;
renderNavigation?: () => React.ReactNode;
}
9 changes: 9 additions & 0 deletions src/navigation/__stories__/CustomButton/CustomButton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.custom-button {
white-space: nowrap;

transition: transform 300ms;

&_active {
transform: scale(0.9);
}
}
25 changes: 25 additions & 0 deletions src/navigation/__stories__/CustomButton/CustomButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

import {TriangleUp} from '@gravity-ui/icons';
import {Button} from '@gravity-ui/uikit';

import {cn} from '../../../utils';
import {NavigationItemProps} from '../../models';

import './CustomButton.scss';

const b = cn('custom-button');

type DCDropdownNavigationItemProps = Pick<NavigationItemProps, 'onClick' | 'isActive'>;

export const CustomButton: React.FC<DCDropdownNavigationItemProps> = (props) => {
const {onClick, isActive} = props;

return (
<Button size="l" view="flat" className={b({active: isActive})} onClick={onClick}>
<Button.Icon>
<TriangleUp height={20} width={20} />
</Button.Icon>
</Button>
);
};
17 changes: 17 additions & 0 deletions src/navigation/__stories__/Navigation.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Meta, StoryFn} from '@storybook/react';
import {PageConstructor} from '../../containers/PageConstructor';
import {CustomConfig, NavigationData} from '../../models';

import {CustomButton} from './CustomButton/CustomButton';
import {CustomComponent} from './CustomComponent/CustomComponent';

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

DefaultNavigation.args = {
navigation: data.navigation as NavigationData,
Expand Down Expand Up @@ -56,3 +58,18 @@ NavigationWithCustomItems.args = {
},
} as NavigationData,
};

NavigationWithCustomMobileHeaderItems.args = {
custom: {
navigation: {
'custom-item': CustomButton,
},
},
navigation: {
...data.navigation,
header: {
...data.navigation.header,
customMobileHeaderItems: [{type: 'custom-item'}],
},
} as unknown as NavigationData,
};
53 changes: 50 additions & 3 deletions src/navigation/components/DesktopNavigation/DesktopNavigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,47 @@ $block: '.#{$ns}desktop-navigation';
}
}

&__mobile-navigation {
@include add-specificity(&) {
justify-content: flex-end;

@include mobile-tablet-only();
}

&-container {
padding-right: $indentXXXS;
}

&-container:has(.#{$ns}overflow-scroller__arrow) & {
justify-content: flex-start;
}

&-container:has(.#{$ns}overflow-scroller__arrow_type_right) {
padding-right: $indentXS;
}
}

&__right {
flex: 0;
justify-content: flex-end;

@include text-size(body-2);

@media (max-width: map-get($gridBreakpoints,'md') - 1) {
flex: 3 0 0;
max-width: 50%;
}
}

&__navigation-container {
&__navigation-container,
&__mobile-navigation-container {
display: flex;
overflow-x: hidden;
flex: 1 0 0;
justify-content: space-between;
align-items: center;
}

&__navigation-container {
margin-right: $indentM;
}

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

&__links,
&__buttons,
&__links {
&__mobile-buttons {
display: flex;
align-items: center;

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

&__mobile-buttons {
@include mobile-tablet-only();
}

&__mobile-buttons &__item {
&:not(:last-child) {
margin-right: 0;
}
}

&__links {
position: relative;

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

&__navigation-container,
&__mobile-navigation-container {
justify-content: flex-end;
}

&__navigation-container {
flex: 0 0 0;
}

&__left {
flex: 1 0 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import './DesktopNavigation.scss';

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

const DesktopNavigation: React.FC<DesktopNavigationProps> = ({
export const DesktopNavigation: React.FC<DesktopNavigationProps> = ({
logo,
leftItemsWithIconSize,
rightItemsWithIconSize,
customMobileHeaderItems,
isSidebarOpened,
onSidebarOpenedChange,
onActiveItemChange,
Expand All @@ -40,6 +41,25 @@ const DesktopNavigation: React.FC<DesktopNavigationProps> = ({
</OverflowScroller>
</div>
<div className={b('right')}>
{customMobileHeaderItems && (
<div className={b('mobile-navigation-container')}>
<OverflowScroller
className={b('mobile-navigation')}
onScrollStart={onActiveItemChange}
arrowSize={18}
>
<NavigationList
items={customMobileHeaderItems}
onActiveItemChange={onActiveItemChange}
className={b('mobile-buttons')}
itemClassName={b('item')}
column={ItemColumnName.Left}
activeItemId={activeItemId}
menuLayout={NavigationLayout.Dropdown}
/>
</OverflowScroller>
</div>
)}
<MobileMenuButton
isSidebarOpened={isSidebarOpened}
onSidebarOpenedChange={onSidebarOpenedChange}
Expand Down
2 changes: 1 addition & 1 deletion src/navigation/components/Logo/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type LogoProps = ThemedNavigationLogoData & {
alt?: string;
};

const Logo: React.FC<LogoProps> = ({alt = i18n('image-alt'), ...restProps}) => {
export const Logo: React.FC<LogoProps> = ({alt = i18n('image-alt'), ...restProps}) => {
const props: LogoProps = {...restProps, alt};
const {hostname, Link} = useContext(LocationContext);
const theme = useTheme();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import './MobileNavigation.scss';

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

const MobileNavigation: React.FC<MobileNavigationProps> = ({
export const MobileNavigation: React.FC<MobileNavigationProps> = ({
isOpened,
topItems,
bottomItems,
Expand Down
48 changes: 10 additions & 38 deletions src/navigation/components/Navigation/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,39 @@
import React, {useEffect, useMemo, useState} from 'react';

import debounce from 'lodash/debounce';
import React, {useState} from 'react';

import OutsideClick from '../../../components/OutsideClick/OutsideClick';
import {Col, Grid, Row} from '../../../grid';
import {ClassNameProps, HeaderData, ThemedNavigationLogoData} from '../../../models';
import {block} from '../../../utils';
import {getNavigationItemWithIconSize} from '../../utils';
import {useActiveNavItem, useShowBorder} from '../../hooks';
import DesktopNavigation from '../DesktopNavigation/DesktopNavigation';
import MobileNavigation from '../MobileNavigation/MobileNavigation';

import './Navigation.scss';

const b = block('navigation');

export interface NavigationProps extends ClassNameProps {
export interface NavigationComponentProps extends ClassNameProps {
logo: ThemedNavigationLogoData;
data: HeaderData;
}

export const Navigation: React.FC<NavigationProps> = ({data, logo, className}) => {
export const Navigation: React.FC<NavigationComponentProps> = ({data, logo, className}) => {
const {
leftItems,
rightItems,
customMobileHeaderItems,
iconSize = 20,
withBorder = false,
withBorderOnScroll = true,
} = data;
const [isSidebarOpened, setIsSidebarOpened] = useState(false);
const [activeItemId, setActiveItemId] = useState<string | undefined>(undefined);
const [showBorder, setShowBorder] = useState(withBorder);

const getNavigationItem = getNavigationItemWithIconSize(iconSize);

const leftItemsWithIconSize = useMemo(
() => leftItems.map(getNavigationItem),
[getNavigationItem, leftItems],
);
const rightItemsWithIconSize = useMemo(
() => rightItems?.map(getNavigationItem),
[getNavigationItem, rightItems],
);

const onActiveItemChange = (id?: string) => {
setActiveItemId(id);
};
const [isSidebarOpened, setIsSidebarOpened] = useState(false);
const [showBorder] = useShowBorder(withBorder, withBorderOnScroll);
const {activeItemId, leftItemsWithIconSize, rightItemsWithIconSize, onActiveItemChange} =
useActiveNavItem(iconSize, leftItems, rightItems);

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

useEffect(() => {
if (!withBorderOnScroll) return () => {};

const showBorderOnScroll = () => {
if (!withBorder) {
setShowBorder(window.scrollY > 0);
}
};

const scrollHandler = debounce(showBorderOnScroll, 20);

window.addEventListener('scroll', scrollHandler, {passive: true});
return () => window.removeEventListener('scroll', scrollHandler);
});

return (
<Grid className={b({'with-border': showBorder}, className)}>
<Row>
Expand All @@ -74,6 +45,7 @@ export const Navigation: React.FC<NavigationProps> = ({data, logo, className}) =
onActiveItemChange={onActiveItemChange}
leftItemsWithIconSize={leftItemsWithIconSize}
rightItemsWithIconSize={rightItemsWithIconSize}
customMobileHeaderItems={customMobileHeaderItems}
isSidebarOpened={isSidebarOpened}
onSidebarOpenedChange={onSidebarOpenedChange}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const nonComplexNavigationItemTypes = NavigationItemTypes.filter(
(type) => type !== NavigationItemType.Dropdown,
);

const NavigationItem: React.FC<NavigationItemProps> = ({
export const NavigationItem: React.FC<NavigationItemProps> = ({
data,
className,
menuLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {NavigationListItemProps} from '../../models';
import {getItemClickHandler} from '../../utils';
import NavigationItem from '../NavigationItem';

const NavigationListItem: React.FC<NavigationListItemProps> = ({
export const NavigationListItem: React.FC<NavigationListItemProps> = ({
column,
index,
activeItemId,
Expand Down
4 changes: 2 additions & 2 deletions src/navigation/components/Standalone/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React from 'react';

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

import Navigation, {NavigationProps} from './../../components/Navigation/Navigation';
import Navigation, {NavigationComponentProps} from './../../components/Navigation/Navigation';

const Standalone = (props: NavigationProps) => (
const Standalone = (props: NavigationComponentProps) => (
<RootCn>
<Navigation {...props} />
</RootCn>
Expand Down
17 changes: 10 additions & 7 deletions src/navigation/containers/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ export interface LayoutProps {

const Layout: React.FC<LayoutProps> = ({children, navigation}) => (
<div className={b()}>
{navigation && (
<Navigation
data={navigation.header}
logo={navigation.logo}
className={b('navigation')}
/>
)}
{navigation &&
(navigation.renderNavigation ? (
navigation.renderNavigation()
) : (
<Navigation
data={navigation.header}
logo={navigation.logo}
className={b('navigation')}
/>
))}
<main className={b('content')}>{children}</main>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/navigation/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {default as useActiveNavItem} from './useActiveNavItem';
export {default as useShowBorder} from './useShowBorder';
Loading

0 comments on commit 6cd9289

Please sign in to comment.