From 9f47dbe3f5ef7f42205c66092ce25879a39d1286 Mon Sep 17 00:00:00 2001 From: Nik Tverd <61203447+niktverd@users.noreply.github.com> Date: Tue, 25 Jul 2023 08:15:46 +0600 Subject: [PATCH] feat: navigation dropdown refactoring (#392) * feat: navigation dropdown refactoring --- src/hooks/useHeightCalculator.ts | 2 +- src/models/navigation.ts | 6 +- .../DesktopNavigation.scss} | 57 +++--- .../DesktopNavigation/DesktopNavigation.tsx | 64 ++++++ src/navigation/components/Header/Header.tsx | 188 ------------------ .../MobileMenuButton/MobileMenuButton.scss | 9 + .../MobileMenuButton/MobileMenuButton.tsx | 28 +++ .../MobileNavigation/MobileNavigation.scss | 20 +- .../MobileNavigation/MobileNavigation.tsx | 163 +++------------ .../components/Navigation/Navigation.scss | 25 +-- .../components/Navigation/Navigation.tsx | 102 ++++++---- .../NavigationDropdownItem.tsx | 45 ----- .../NavigationItem/NavigationItem.scss | 38 ++++ .../NavigationItem/NavigationItem.tsx | 70 ++++--- .../components/GithubButton/GithubButton.tsx | 2 +- .../NavigationButton/NavigationButton.tsx | 2 +- .../NavigationDropdown/NavigationDropdown.tsx | 44 ++-- .../NavigationLink/NavigationLink.tsx | 2 +- .../NavigationList/NavigationList.tsx | 25 +++ .../NavigationListItem.scss | 34 ---- .../NavigationListItem/NavigationListItem.tsx | 54 ++--- .../NavigationPopup/NavigationPopup.scss | 8 +- .../NavigationPopup/NavigationPopup.tsx | 53 +++-- src/navigation/constants.ts | 6 - src/navigation/containers/Layout/Layout.tsx | 4 +- src/navigation/models.ts | 88 ++++++++ src/navigation/utils.ts | 35 +++- 27 files changed, 548 insertions(+), 626 deletions(-) rename src/navigation/components/{Header/Header.scss => DesktopNavigation/DesktopNavigation.scss} (84%) create mode 100644 src/navigation/components/DesktopNavigation/DesktopNavigation.tsx delete mode 100644 src/navigation/components/Header/Header.tsx create mode 100644 src/navigation/components/MobileMenuButton/MobileMenuButton.scss create mode 100644 src/navigation/components/MobileMenuButton/MobileMenuButton.tsx delete mode 100644 src/navigation/components/NavigationDropdownItem/NavigationDropdownItem.tsx create mode 100644 src/navigation/components/NavigationItem/NavigationItem.scss create mode 100644 src/navigation/components/NavigationList/NavigationList.tsx delete mode 100644 src/navigation/components/NavigationListItem/NavigationListItem.scss delete mode 100644 src/navigation/constants.ts create mode 100644 src/navigation/models.ts diff --git a/src/hooks/useHeightCalculator.ts b/src/hooks/useHeightCalculator.ts index 65d4afef2..3d9c578b8 100644 --- a/src/hooks/useHeightCalculator.ts +++ b/src/hooks/useHeightCalculator.ts @@ -26,7 +26,7 @@ const useHeightCalculator: ( useEffect(() => { const handleResize = _.debounce(calculateContainerHeight, recalculateOnResizeDelay); - calculateContainerHeight(); + handleResize(); window.addEventListener('resize', handleResize, {passive: true}); return () => { diff --git a/src/models/navigation.ts b/src/models/navigation.ts index 199ff0f31..e7652e7c1 100644 --- a/src/models/navigation.ts +++ b/src/models/navigation.ts @@ -52,6 +52,8 @@ export interface NavigationButtonItem extends ButtonProps { export interface NavigationDropdownItem extends NavigationItemBase { type: NavigationItemType.Dropdown; items: NavigationLinkItem[]; + hidePopup: () => void; + isActive: boolean; } export interface NavigationSocialItem extends Omit { @@ -69,9 +71,7 @@ export type NavigationItemData = | NavigationLinkItem | NavigationButtonItem | NavigationSocialItem - | DropdownItemData; - -export type DropdownItemData = Omit; + | NavigationDropdownItem; export interface NavigationLogoData { icon: ImageProps; diff --git a/src/navigation/components/Header/Header.scss b/src/navigation/components/DesktopNavigation/DesktopNavigation.scss similarity index 84% rename from src/navigation/components/Header/Header.scss rename to src/navigation/components/DesktopNavigation/DesktopNavigation.scss index 22422cd69..3887fbc64 100644 --- a/src/navigation/components/Header/Header.scss +++ b/src/navigation/components/DesktopNavigation/DesktopNavigation.scss @@ -1,27 +1,9 @@ @import '../../../../styles/variables'; @import '../../../../styles/mixins'; -$block: '.#{$ns}header'; +$block: '.#{$ns}desktop-navigation'; #{$block} { - $root: &; - - position: sticky; - z-index: 98; - top: 0; - - display: flex; - justify-content: center; - align-items: center; - - height: var(--header-height); - - background-color: var(--yc-color-base-background); - - &_with-border { - box-shadow: inset 0 -1px 0 var(--yc-color-line-generic); - } - &__wrapper { display: flex; justify-content: space-between; @@ -76,11 +58,26 @@ $block: '.#{$ns}header'; margin-right: $indentM; } - &__buttons { + &__button { + margin-top: 0; + } + + &__logo { + margin: 0 $indentM 0 0; + + cursor: pointer; + } + + &__buttons, + &__links { display: flex; align-items: center; - @include desktop-only(); + @include reset-list-style(); + } + + &__buttons { + @include desktop-only(); & > * { &:not(:last-child) { @@ -89,22 +86,18 @@ $block: '.#{$ns}header'; } } - &__buttons-item { + &__links { position: relative; - &:not(:last-child) { - margin-right: $indentS; - } - } - - &__button { - margin-top: 0; + @include text-size(body-2); } - &__logo { - margin: 0 $indentM 0 0; + &__item { + position: relative; - cursor: pointer; + &:not(:last-child) { + margin-right: $indentS; + } } @media (max-width: map-get($gridBreakpoints, 'md') - 1) { diff --git a/src/navigation/components/DesktopNavigation/DesktopNavigation.tsx b/src/navigation/components/DesktopNavigation/DesktopNavigation.tsx new file mode 100644 index 000000000..a73562c87 --- /dev/null +++ b/src/navigation/components/DesktopNavigation/DesktopNavigation.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import _ from 'lodash'; + +import OverflowScroller from '../../../components/OverflowScroller/OverflowScroller'; +import {block} from '../../../utils'; +import {DesktopNavigationProps, ItemColumnName, NavigationLayout} from '../../models'; +import Logo from '../Logo/Logo'; +import {MobileMenuButton} from '../MobileMenuButton/MobileMenuButton'; +import {NavigationList} from '../NavigationList/NavigationList'; + +import './DesktopNavigation.scss'; + +const b = block('desktop-navigation'); + +const DesktopNavigation: React.FC = ({ + logo, + leftItemsWithIconSize, + rightItemsWithIconSize, + isSidebarOpened, + onSidebarOpenedChange, + onActiveItemChange, + activeItemId, +}) => ( +
+ {logo && ( +
+ +
+ )} +
+ + + +
+
+ + {rightItemsWithIconSize && ( + + )} +
+
+); + +export default DesktopNavigation; diff --git a/src/navigation/components/Header/Header.tsx b/src/navigation/components/Header/Header.tsx deleted file mode 100644 index c4559688c..000000000 --- a/src/navigation/components/Header/Header.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import React, {MouseEvent, useCallback, useEffect, useMemo, useState} from 'react'; - -import _ from 'lodash'; - -import Control from '../../../components/Control/Control'; -import OutsideClick from '../../../components/OutsideClick/OutsideClick'; -import {Col, Grid, Row} from '../../../grid'; -import {NavigationClose, NavigationOpen} from '../../../icons'; -import { - HeaderData, - NavigationButtonItem, - NavigationDropdownItem, - NavigationItemBase, - NavigationItemModel, - NavigationItemType, - NavigationLinkItem, - ThemedNavigationLogoData, -} from '../../../models'; -import {block} from '../../../utils'; -import {ItemColumnName} from '../../constants'; -import Logo from '../Logo/Logo'; -import MobileNavigation from '../MobileNavigation/MobileNavigation'; -import Navigation from '../Navigation/Navigation'; -import {NavigationListItem} from '../NavigationListItem/NavigationListItem'; - -import './Header.scss'; - -const b = block('header'); - -const ICON_SIZE = 36; - -export interface HeaderProps { - logo: ThemedNavigationLogoData; - data: HeaderData; -} - -interface MobileMenuButtonProps { - isSidebarOpened: boolean; - onSidebarOpenedChange: (arg: boolean) => void; -} - -const MobileMenuButton: React.FC = ({ - isSidebarOpened, - onSidebarOpenedChange, -}) => { - const iconProps = { - icon: isSidebarOpened ? NavigationClose : NavigationOpen, - iconSize: ICON_SIZE, - }; - - return ( - { - e.stopPropagation(); - onSidebarOpenedChange(!isSidebarOpened); - }} - size="l" - {...iconProps} - /> - ); -}; - -const iconSizeKey: keyof NavigationItemBase = 'iconSize'; - -const isButtonItem = (item: NavigationItemModel): item is NavigationButtonItem => - item.type === NavigationItemType.Button; - -const isDropdownItem = (item: NavigationItemModel): item is NavigationDropdownItem => - item.type === NavigationItemType.Dropdown; - -export const Header: React.FC = ({data, logo}) => { - const {leftItems, rightItems, iconSize = 20, withBorder = false} = data; - const [isSidebarOpened, setIsSidebarOpened] = useState(false); - const [activeItemId, setActiveItemId] = useState(undefined); - const [showBorder, setShowBorder] = useState(withBorder); - - const getNavigationItemWithIconSize = useCallback( - (item: NavigationItemModel) => { - const newItem = {...item}; - if ('items' in newItem && isDropdownItem(newItem)) { - newItem.items = newItem.items.map( - getNavigationItemWithIconSize, - ) as NavigationLinkItem[]; - } - - if (!(iconSizeKey in newItem) && !isButtonItem(newItem)) { - newItem.iconSize = iconSize; - } - return newItem; - }, - [iconSize], - ); - - const leftItemsWithIconSize = useMemo( - () => leftItems.map(getNavigationItemWithIconSize), - [getNavigationItemWithIconSize, leftItems], - ); - const rightItemsWithIconSize = useMemo( - () => rightItems?.map(getNavigationItemWithIconSize), - [getNavigationItemWithIconSize, rightItems], - ); - - const onActiveItemChange = useCallback((id?: string) => { - setActiveItemId(id); - }, []); - - const hidePopup = useCallback(() => { - onActiveItemChange(); - }, [onActiveItemChange]); - - const onSidebarOpenedChange = useCallback((isOpen: boolean) => { - setIsSidebarOpened(isOpen); - }, []); - - const hideSidebar = useCallback(() => { - setIsSidebarOpened(false); - }, []); - - useEffect(() => { - const showBorderOnScroll = () => { - if (!showBorder) { - setShowBorder(window.scrollY > 0); - } - }; - - window.addEventListener('scroll', _.debounce(showBorderOnScroll, 20), {passive: true}); - return () => window.removeEventListener('scroll', _.debounce(showBorderOnScroll, 20)); - }); - - return ( - - - -
- {logo && ( -
- -
- )} -
- -
-
- - {rightItemsWithIconSize && ( -
    - {rightItemsWithIconSize.map((button, index) => ( - - ))} -
- )} -
- onSidebarOpenedChange(false)}> - - -
- -
-
- ); -}; - -export default Header; diff --git a/src/navigation/components/MobileMenuButton/MobileMenuButton.scss b/src/navigation/components/MobileMenuButton/MobileMenuButton.scss new file mode 100644 index 000000000..709aa469e --- /dev/null +++ b/src/navigation/components/MobileMenuButton/MobileMenuButton.scss @@ -0,0 +1,9 @@ +@import '../../../../styles/mixins'; + +$block: '.#{$ns}mobile-menu-button'; + +#{$block} { + @include add-specificity(&) { + @include mobile-tablet-only(); + } +} diff --git a/src/navigation/components/MobileMenuButton/MobileMenuButton.tsx b/src/navigation/components/MobileMenuButton/MobileMenuButton.tsx new file mode 100644 index 000000000..4b0a7b7bd --- /dev/null +++ b/src/navigation/components/MobileMenuButton/MobileMenuButton.tsx @@ -0,0 +1,28 @@ +import React, {MouseEvent} from 'react'; + +import {Control} from '../../../components'; +import {NavigationClose, NavigationOpen} from '../../../icons'; +import {block} from '../../../utils'; +import {MobileMenuButtonProps} from '../../models'; + +import './MobileMenuButton.scss'; + +const b = block('mobile-menu-button'); + +const ICON_SIZE = 36; + +export const MobileMenuButton: React.FC = ({ + isSidebarOpened, + onSidebarOpenedChange, +}) => ( + { + e.stopPropagation(); + onSidebarOpenedChange(!isSidebarOpened); + }} + size="l" + icon={isSidebarOpened ? NavigationClose : NavigationOpen} + iconSize={ICON_SIZE} + /> +); diff --git a/src/navigation/components/MobileNavigation/MobileNavigation.scss b/src/navigation/components/MobileNavigation/MobileNavigation.scss index 023c4ec05..cad4dc441 100644 --- a/src/navigation/components/MobileNavigation/MobileNavigation.scss +++ b/src/navigation/components/MobileNavigation/MobileNavigation.scss @@ -15,6 +15,15 @@ $block: '.#{$ns}mobile-navigation'; border-bottom-left-radius: $borderRadius; background-color: var(--yc-color-base-background); box-shadow: 0 3px 10px var(--yc-color-sfx-shadow); + max-height: calc(100vh - 2 * var(--header-height)); + overflow-y: scroll; + + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } @include text-size(body-2); @include mobile-tablet-only(); @@ -27,10 +36,6 @@ $block: '.#{$ns}mobile-navigation'; margin-top: $indentSM; } - &__nav { - margin-bottom: $indentSM; - } - &__rows { position: relative; @@ -39,12 +44,11 @@ $block: '.#{$ns}mobile-navigation'; padding-bottom: $indentSM; @include reset-list-style(); + margin-bottom: $indentSM; } - &__rows-item { - &:not(:last-child) { - margin-bottom: $indentSM; - } + &__rows:last-child { + margin-bottom: 0; } &__dropdown-item { diff --git a/src/navigation/components/MobileNavigation/MobileNavigation.tsx b/src/navigation/components/MobileNavigation/MobileNavigation.tsx index dcb8e94ad..1728ed6e9 100644 --- a/src/navigation/components/MobileNavigation/MobileNavigation.tsx +++ b/src/navigation/components/MobileNavigation/MobileNavigation.tsx @@ -1,157 +1,48 @@ -import React, {MouseEventHandler, useCallback, useRef} from 'react'; +import React from 'react'; -import {Popup, Portal} from '@gravity-ui/uikit'; +import {Portal} from '@gravity-ui/uikit'; import Foldable from '../../../components/Foldable/Foldable'; -import {NavigationDropdownItem, NavigationItemModel, NavigationItemType} from '../../../models'; import {block} from '../../../utils'; -import {ItemColumnName} from '../../constants'; -import NavigationItem from '../NavigationItem/NavigationItem'; +import {ItemColumnName, MobileNavigationProps, NavigationLayout} from '../../models'; +import {NavigationList} from '../NavigationList/NavigationList'; import './MobileNavigation.scss'; const b = block('mobile-navigation'); -interface MobileNavigationDropdownProps { - data: NavigationDropdownItem; - isOpened?: boolean; - onItemClick?: MouseEventHandler; - onToggle?: MouseEventHandler; -} - -const MobileNavigationDropdown: React.FC = ({ - data, - onItemClick, - onToggle, - isOpened = false, -}) => { - const ref = useRef(null); - - return ( -
- - {isOpened && ( - - {data.items.map((item) => ( - - ))} - - )} -
- ); -}; - -interface MobileNavigationItemProps - extends Pick { - item: NavigationItemModel; - column: ItemColumnName; - index: number; - isOpened?: boolean; - activeItemId?: string; -} - -const MobileNavigationItem = ({ - item, - index, +const MobileNavigation: React.FC = ({ isOpened, - activeItemId, - onActiveItemChange, - column, - onClose, -}: MobileNavigationItemProps) => { - const id = `${column}-${index}`; - const isActive = id === activeItemId && isOpened; - const toggleActive: MouseEventHandler = useCallback( - (e) => { - e.stopPropagation(); - - if (onActiveItemChange) { - onActiveItemChange(isActive ? undefined : `${column}-${index}`); - } - }, - [onActiveItemChange, isActive, column, index], - ); - - const onItemClick: MouseEventHandler = useCallback( - (e) => { - toggleActive(e); - onClose(); - }, - [toggleActive, onClose], - ); - - return ( -
  • - {item.type === NavigationItemType.Dropdown ? ( - - ) : ( - - )} -
  • - ); -}; - -export interface MobileNavigationProps { - className?: string; - isOpened?: boolean; - topItems?: NavigationItemModel[]; - bottomItems?: NavigationItemModel[]; - activeItemId?: string; - onClose: () => void; - onActiveItemChange: (id?: string) => void; -} - -const MobileNavigation: React.FC = (props) => { + topItems, + bottomItems, + ...props +}) => { if (typeof window === 'undefined') { return null; } - const {isOpened, topItems, bottomItems, activeItemId, onActiveItemChange, onClose} = props; - return (
    - -
      - {bottomItems?.map((item, index) => ( - - ))} -
    + {topItems && ( + + )} + {bottomItems && ( + + )}
    diff --git a/src/navigation/components/Navigation/Navigation.scss b/src/navigation/components/Navigation/Navigation.scss index 27043b3c1..0fc52c4a8 100644 --- a/src/navigation/components/Navigation/Navigation.scss +++ b/src/navigation/components/Navigation/Navigation.scss @@ -4,21 +4,22 @@ $block: '.#{$ns}navigation'; #{$block} { - @include text-size(body-2); + $root: &; + $navigationZIndex: 98; - &__links { - position: relative; + position: sticky; + z-index: $navigationZIndex; + top: 0; - display: flex; - align-items: center; - @include reset-list-style(); - } + display: flex; + justify-content: center; + align-items: center; + + height: var(--header-height); - &__links-item { - position: relative; + background-color: var(--yc-color-base-background); - &:not(:last-child) { - margin-right: $indentS; - } + &_with-border { + box-shadow: inset 0 -1px 0 var(--yc-color-line-generic); } } diff --git a/src/navigation/components/Navigation/Navigation.tsx b/src/navigation/components/Navigation/Navigation.tsx index 8cf5b76db..7f50afb1b 100644 --- a/src/navigation/components/Navigation/Navigation.tsx +++ b/src/navigation/components/Navigation/Navigation.tsx @@ -1,61 +1,87 @@ -import React, {useCallback, useContext, useEffect} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import _ from 'lodash'; -import OverflowScroller from '../../../components/OverflowScroller/OverflowScroller'; -import {LocationContext} from '../../../context/locationContext'; -import {NavigationItemModel} from '../../../models/navigation'; +import OutsideClick from '../../../components/OutsideClick/OutsideClick'; +import {Col, Grid, Row} from '../../../grid'; +import {HeaderData, ThemedNavigationLogoData} from '../../../models'; import {block} from '../../../utils'; -import {ItemColumnName} from '../../constants'; -import {NavigationListItem} from '../NavigationListItem/NavigationListItem'; +import {getNavigationItemWithIconSize} from '../../utils'; +import DesktopNavigation from '../DesktopNavigation/DesktopNavigation'; +import MobileNavigation from '../MobileNavigation/MobileNavigation'; import './Navigation.scss'; const b = block('navigation'); export interface NavigationProps { - links: NavigationItemModel[]; - activeItemId?: string; - className?: string; - highlightActiveItem?: boolean; - onActiveItemChange: (id?: string) => void; + logo: ThemedNavigationLogoData; + data: HeaderData; } -const Navigation: React.FC = ({ - className, - onActiveItemChange, - links, - activeItemId, -}) => { - const {asPath, pathname} = useContext(LocationContext); +export const Navigation: React.FC = ({data, logo}) => { + const {leftItems, rightItems, iconSize = 20, withBorder = false} = data; + const [isSidebarOpened, setIsSidebarOpened] = useState(false); + const [activeItemId, setActiveItemId] = useState(undefined); + const [showBorder, setShowBorder] = useState(withBorder); - const hidePopup = useCallback(() => { - onActiveItemChange(); - }, [onActiveItemChange]); + 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 onSidebarOpenedChange = (isOpen: boolean) => setIsSidebarOpened(isOpen); useEffect(() => { - hidePopup(); - }, [hidePopup, asPath, pathname]); + const showBorderOnScroll = () => { + if (!showBorder) { + setShowBorder(window.scrollY > 0); + } + }; + + const scrollHandler = _.debounce(showBorderOnScroll, 20); + + window.addEventListener('scroll', scrollHandler, {passive: true}); + return () => window.removeEventListener('scroll', scrollHandler); + }); return ( - - - + onSidebarOpenedChange(false)}> + + + + + + ); }; diff --git a/src/navigation/components/NavigationDropdownItem/NavigationDropdownItem.tsx b/src/navigation/components/NavigationDropdownItem/NavigationDropdownItem.tsx deleted file mode 100644 index b31ab19e7..000000000 --- a/src/navigation/components/NavigationDropdownItem/NavigationDropdownItem.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, {Fragment, MouseEventHandler, useRef} from 'react'; - -import {NavigationDropdownItem, NavigationItemType} from '../../../models'; -import NavigationItem from '../NavigationItem/NavigationItem'; -import NavigationPopup from '../NavigationPopup/NavigationPopup'; - -export interface NavigationDropdownProps { - className?: string; - data: NavigationDropdownItem; - onClick: MouseEventHandler; - isActive: boolean; - hidePopup: () => void; -} - -const NavigationDropdown: React.FC = ({ - className, - data, - isActive, - hidePopup, - onClick, -}) => { - const anchorRef = useRef(null); - const {text, icon, items, iconSize, ...popupProps} = data; - - return ( - - - - - ); -}; - -export default NavigationDropdown; diff --git a/src/navigation/components/NavigationItem/NavigationItem.scss b/src/navigation/components/NavigationItem/NavigationItem.scss new file mode 100644 index 000000000..cc8810854 --- /dev/null +++ b/src/navigation/components/NavigationItem/NavigationItem.scss @@ -0,0 +1,38 @@ +@import '../../../../styles/variables'; +@import '../../../../styles/mixins'; + +$block: '.#{$ns}navigation-item'; + +#{$block} { + cursor: pointer; + + &:last-child { + margin-bottom: 0; + } + + @include islands-focus(); + @include reset-link-style(); + + &__content { + &:hover, + &:active { + color: var(--yc-color-text-link); + } + } + + &_menu-layout { + &_desktop { + height: var(--header-height); + line-height: var(--header-height); + margin-bottom: 0; + } + + &_mobile { + margin-bottom: $indentSM; + } + + &_dropdown { + margin-bottom: 0; + } + } +} diff --git a/src/navigation/components/NavigationItem/NavigationItem.tsx b/src/navigation/components/NavigationItem/NavigationItem.tsx index 82e6fd9c2..e3ebe2398 100644 --- a/src/navigation/components/NavigationItem/NavigationItem.tsx +++ b/src/navigation/components/NavigationItem/NavigationItem.tsx @@ -1,7 +1,11 @@ -import React, {MouseEventHandler, useMemo} from 'react'; +import React, {useMemo} from 'react'; + +import {omit} from 'lodash'; import {BlockIdContext} from '../../../context/blockIdContext'; -import {NavigationItemData, NavigationItemType} from '../../../models'; +import {NavigationItemType} from '../../../models'; +import {block} from '../../../utils'; +import {NavigationItemProps} from '../../models'; import SocialIcon from '../SocialIcon/SocialIcon'; import {GithubButton} from './components/GithubButton/GithubButton'; @@ -9,14 +13,11 @@ import {NavigationButton} from './components/NavigationButton/NavigationButton'; import {NavigationDropdown} from './components/NavigationDropdown/NavigationDropdown'; import {NavigationLink} from './components/NavigationLink/NavigationLink'; -const ANALYTICS_ID = 'navigation'; +import './NavigationItem.scss'; -export interface NavigationItemProps { - data: NavigationItemData; - className?: string; - onClick?: MouseEventHandler; - isOpened?: boolean; -} +const b = block('navigation-item'); + +const ANALYTICS_ID = 'navigation'; //todo: add types support form component in map // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -28,27 +29,34 @@ const NavigationItemsMap: Record> = [NavigationItemType.GithubButton]: GithubButton, }; -const NavigationItem = React.forwardRef( - ({data, className, ...props}, ref) => { - const {type = NavigationItemType.Link} = data; - const Component = NavigationItemsMap[type]; - const componentProps = useMemo( - () => ({ - className, - ...data, - ...props, - ref, - }), - [className, data, props, ref], - ); - - return ( - - - - ); - }, -); -NavigationItem.displayName = 'NavigationItem'; +const NavigationItem: React.FC = ({ + data, + className, + menuLayout, + ...props +}: NavigationItemProps) => { + const {type = NavigationItemType.Link} = data; + const Component = NavigationItemsMap[type]; + const componentProps = useMemo(() => { + const componentProperties = { + ...data, + ...props, + }; + + if (type !== NavigationItemType.Dropdown) { + return omit(componentProperties, 'hidePopup', 'isActive'); + } + + return componentProperties; + }, [data, props, type]); + + return ( + +
  • + +
  • +
    + ); +}; export default NavigationItem; diff --git a/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx b/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx index 340b00801..626b5599e 100644 --- a/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx +++ b/src/navigation/components/NavigationItem/components/GithubButton/GithubButton.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef} from 'react'; import {NavigationGithubButton, NavigationGithubButtonIcon} from '../../../../../models'; import {block} from '../../../../../utils'; -import {NavigationItemProps} from '../../NavigationItem'; +import {NavigationItemProps} from '../../../../models'; import './GithubButton.scss'; diff --git a/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx b/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx index e5ec3f9b5..e5b592afb 100644 --- a/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx +++ b/src/navigation/components/NavigationItem/components/NavigationButton/NavigationButton.tsx @@ -4,7 +4,7 @@ import {Button, RouterLink} from '../../../../../components'; import {BlockIdContext} from '../../../../../context/blockIdContext'; import {ButtonProps} from '../../../../../models'; import {block} from '../../../../../utils'; -import {NavigationItemProps} from '../../NavigationItem'; +import {NavigationItemProps} from '../../../../models'; import './NavigationButton.scss'; diff --git a/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx b/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx index ec859f44e..717070934 100644 --- a/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx +++ b/src/navigation/components/NavigationItem/components/NavigationDropdown/NavigationDropdown.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, {Fragment, useRef} from 'react'; import {ToggleArrow} from '../../../../../components'; import {getMediaImage} from '../../../../../components/Media/Image/utils'; -import {DropdownItemData} from '../../../../../models'; +import {NavigationDropdownItem} from '../../../../../models'; import {block} from '../../../../../utils'; -import {NavigationItemProps} from '../../NavigationItem'; +import {NavigationItemProps} from '../../../../models'; +import NavigationPopup from '../../../NavigationPopup/NavigationPopup'; import {ContentWrapper} from '../ContentWrapper/ContentWrapper'; import './NavigationDropdown.scss'; @@ -13,24 +14,39 @@ const b = block('navigation-dropdown'); const TOGGLE_ARROW_SIZE = 12; -type NavigationDropdownProps = NavigationItemProps & DropdownItemData; +type NavigationDropdownProps = NavigationItemProps & NavigationDropdownItem; -export const NavigationDropdown = React.forwardRef( - ({text, icon, isOpened, className, iconSize, onClick}, ref) => { - const iconData = icon && getMediaImage(icon); +export const NavigationDropdown = ({ + text, + icon, + className, + iconSize, + hidePopup, + items, + isActive, + ...props +}: NavigationDropdownProps) => { + const iconData = icon && getMediaImage(icon); + const anchorRef = useRef(null); - return ( - + return ( + + - ); - }, -); -NavigationDropdown.displayName = 'NavigationDropdown'; + + + ); +}; diff --git a/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx b/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx index 0c95a5866..002891698 100644 --- a/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx +++ b/src/navigation/components/NavigationItem/components/NavigationLink/NavigationLink.tsx @@ -6,7 +6,7 @@ import {LocationContext} from '../../../../../context/locationContext'; import {NavigationArrow} from '../../../../../icons'; import {NavigationLinkItem} from '../../../../../models'; import {block, getLinkProps} from '../../../../../utils'; -import {NavigationItemProps} from '../../NavigationItem'; +import {NavigationItemProps} from '../../../../models'; import {ContentWrapper} from '../ContentWrapper/ContentWrapper'; import './NavigationLink.scss'; diff --git a/src/navigation/components/NavigationList/NavigationList.tsx b/src/navigation/components/NavigationList/NavigationList.tsx new file mode 100644 index 000000000..4b2ad97d7 --- /dev/null +++ b/src/navigation/components/NavigationList/NavigationList.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import _ from 'lodash'; + +import {NavigationListProps} from '../../models'; +import NavigationListItem from '../NavigationListItem/NavigationListItem'; + +export const NavigationList: React.FC = ({ + className, + itemClassName, + items, + ...props +}) => ( +
      + {items.map((item, index) => ( + + ))} +
    +); diff --git a/src/navigation/components/NavigationListItem/NavigationListItem.scss b/src/navigation/components/NavigationListItem/NavigationListItem.scss deleted file mode 100644 index 931f0b423..000000000 --- a/src/navigation/components/NavigationListItem/NavigationListItem.scss +++ /dev/null @@ -1,34 +0,0 @@ -@import '../../../../styles/variables'; -@import '../../../../styles/mixins'; - -$block: '.#{$ns}navigation-list-item'; - -#{$block} { - height: var(--header-height); - line-height: var(--header-height); - cursor: pointer; - - @include islands-focus(); - @include reset-link-style(); - - &__content { - &:hover, - &:active { - color: var(--yc-color-text-link); - } - } - - &__slider-container { - position: absolute; - right: 0; - bottom: 0; - left: 0; - } - - &__slider { - width: 100%; - height: 2px; - - background-color: var(--yc-color-text-link); - } -} diff --git a/src/navigation/components/NavigationListItem/NavigationListItem.tsx b/src/navigation/components/NavigationListItem/NavigationListItem.tsx index 9f0ebba22..1b5f38a2e 100644 --- a/src/navigation/components/NavigationListItem/NavigationListItem.tsx +++ b/src/navigation/components/NavigationListItem/NavigationListItem.tsx @@ -1,35 +1,15 @@ import React from 'react'; -import {ClassNameProps, NavigationItemModel, NavigationItemType} from '../../../models'; -import {block} from '../../../utils'; -import {ItemColumnName} from '../../constants'; +import {NavigationListItemProps} from '../../models'; import {getItemClickHandler} from '../../utils'; -import NavigationDropdownItem from '../NavigationDropdownItem/NavigationDropdownItem'; import NavigationItem from '../NavigationItem/NavigationItem'; -import './NavigationListItem.scss'; - -const b = block('navigation-list-item'); - -type NavigationListItemProps = { - item: NavigationItemModel; - index: number; - column: ItemColumnName; - activeItemId?: string; - highlightActiveItem?: boolean; - hidePopup: () => void; - onActiveItemChange: (id?: string) => void; -} & ClassNameProps; - -export const NavigationListItem = ({ - item, - className, +const NavigationListItem: React.FC = ({ + column, index, activeItemId, - highlightActiveItem, - hidePopup, - column, onActiveItemChange, + ...props }: NavigationListItemProps) => { const id = `${column}-${index}`; const isActive = id === activeItemId; @@ -41,23 +21,13 @@ export const NavigationListItem = ({ }); return ( -
  • - {item.type === NavigationItemType.Dropdown ? ( - - ) : ( - - )} - {highlightActiveItem && isActive && ( -
    -
    -
    - )} -
  • + ); }; + +export default NavigationListItem; diff --git a/src/navigation/components/NavigationPopup/NavigationPopup.scss b/src/navigation/components/NavigationPopup/NavigationPopup.scss index 74f0e79cb..e0d756799 100644 --- a/src/navigation/components/NavigationPopup/NavigationPopup.scss +++ b/src/navigation/components/NavigationPopup/NavigationPopup.scss @@ -7,13 +7,17 @@ $block: '.#{$ns}navigation-popup'; $offset: calc($gridGutter * 2 * -1); margin-left: $offset; - margin-top: -#{$indentXS}; + margin-top: $offset; - @include desktop-only(); @include navigation-popup(); + &__list { + @include reset-list-style(); + } + &__link { height: 36px; + line-height: 20px; padding: $indentXXXS $indentXXS; border-radius: 8px; diff --git a/src/navigation/components/NavigationPopup/NavigationPopup.tsx b/src/navigation/components/NavigationPopup/NavigationPopup.tsx index d805384de..b08b8fb09 100644 --- a/src/navigation/components/NavigationPopup/NavigationPopup.tsx +++ b/src/navigation/components/NavigationPopup/NavigationPopup.tsx @@ -2,8 +2,8 @@ import React from 'react'; import {Popup} from '@gravity-ui/uikit'; -import {NavigationLinkItem} from '../../../models'; import {block} from '../../../utils'; +import {NavigationLayout, NavigationPopupProps} from '../../models'; import NavigationItem from '../NavigationItem/NavigationItem'; import './NavigationPopup.scss'; @@ -11,40 +11,37 @@ import './NavigationPopup.scss'; const b = block('navigation-popup'); const OFFSET_RESET: [number, number] = [0, 0]; -export interface NavigationPopupProps { - open: boolean; - items: NavigationLinkItem[]; - onClose: () => void; - className?: string; - anchorRef: React.RefObject; -} - export const NavigationPopup: React.FC = ({ anchorRef, items, onClose, className, open, -}) => { - return ( - +}) => ( + +
      {items.map((item) => ( - + ))} - - ); -}; +
    +
    +); export default NavigationPopup; diff --git a/src/navigation/constants.ts b/src/navigation/constants.ts deleted file mode 100644 index b48f99d63..000000000 --- a/src/navigation/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum ItemColumnName { - Left = 'left', - Right = 'right', - Top = 'top', - Bottom = 'bottom', -} diff --git a/src/navigation/containers/Layout/Layout.tsx b/src/navigation/containers/Layout/Layout.tsx index b56ac415e..14925937c 100644 --- a/src/navigation/containers/Layout/Layout.tsx +++ b/src/navigation/containers/Layout/Layout.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {NavigationData} from '../../../models'; import {block} from '../../../utils'; -import Header from '../../components/Header/Header'; +import Navigation from '../../components/Navigation/Navigation'; import './Layout.scss'; @@ -15,7 +15,7 @@ export interface LayoutProps { const Layout: React.FC = ({children, navigation}) => (
    - {navigation &&
    } + {navigation && }
    {children}
    ); diff --git a/src/navigation/models.ts b/src/navigation/models.ts new file mode 100644 index 000000000..4297eb016 --- /dev/null +++ b/src/navigation/models.ts @@ -0,0 +1,88 @@ +import {MouseEventHandler} from 'react'; + +import { + ClassNameProps, + NavigationItemData, + NavigationItemModel, + NavigationLinkItem, + ThemedNavigationLogoData, +} from '../models'; + +export interface MobileMenuButtonProps { + isSidebarOpened: boolean; + onSidebarOpenedChange: (arg: boolean) => void; +} + +export enum ItemColumnName { + Left = 'left', + Right = 'right', + Top = 'top', + Bottom = 'bottom', +} + +export enum NavigationLayout { + Desktop = 'desktop', + Mobile = 'mobile', + Dropdown = 'dropdown', +} + +export interface ActiveItemProps { + activeItemId?: string; + onActiveItemChange: (id?: string) => void; +} + +export interface MenuLayoutProps { + menuLayout?: NavigationLayout; +} +export interface NavigationItemProps extends ClassNameProps, MenuLayoutProps { + data: NavigationItemData; + onClick?: MouseEventHandler; + isActive?: boolean; + isTopLevel?: boolean; + hidePopup?: () => void; +} + +export interface NavigationListItemProps extends MenuLayoutProps, ActiveItemProps, ClassNameProps { + data: NavigationItemModel; + column: ItemColumnName; + index: number; +} + +export interface NavigationListProps + extends Pick, + MenuLayoutProps, + ActiveItemProps, + ClassNameProps { + items: NavigationItemModel[]; + itemClassName?: string; +} + +export interface ItemsWrapperProps + extends Pick, + ActiveItemProps, + ClassNameProps {} + +export interface DesktopNavigationProps extends MobileMenuButtonProps, ActiveItemProps { + logo: ThemedNavigationLogoData; + leftItemsWithIconSize: NavigationItemModel[]; + rightItemsWithIconSize?: NavigationItemModel[]; +} + +export interface MobileNavigationProps extends ClassNameProps, ActiveItemProps { + isOpened?: boolean; + topItems?: NavigationItemModel[]; + bottomItems?: NavigationItemModel[]; +} + +export interface NavigationProps extends MobileMenuButtonProps, ActiveItemProps { + logo: ThemedNavigationLogoData; + leftItemsWithIconSize: NavigationItemModel[]; + rightItemsWithIconSize?: NavigationItemModel[]; +} + +export interface NavigationPopupProps extends ClassNameProps { + open: boolean; + items: NavigationLinkItem[]; + onClose: () => void; + anchorRef: React.RefObject; +} diff --git a/src/navigation/utils.ts b/src/navigation/utils.ts index 863fb95bb..847b29064 100644 --- a/src/navigation/utils.ts +++ b/src/navigation/utils.ts @@ -1,6 +1,15 @@ import {MouseEventHandler} from 'react'; -import {ItemColumnName} from './constants'; +import { + NavigationButtonItem, + NavigationDropdownItem, + NavigationItemBase, + NavigationItemModel, + NavigationItemType, + NavigationLinkItem, +} from '../models'; + +import {ItemColumnName} from './models'; type GetItemClickHandlerArgs = { column: ItemColumnName; @@ -22,3 +31,27 @@ export const getItemClickHandler: ({ } onActiveItemChange(id === activeItemId ? undefined : `${column}-${index}`); }; + +const isButtonItem = (item: NavigationItemModel): item is NavigationButtonItem => + item.type === NavigationItemType.Button; + +const isDropdownItem = (item: NavigationItemModel): item is NavigationDropdownItem => + item.type === NavigationItemType.Dropdown; + +const iconSizeKey: keyof NavigationItemBase = 'iconSize'; + +export function getNavigationItemWithIconSize(iconSize = 20) { + const getItem = (item: NavigationItemModel) => { + const newItem = {...item}; + if ('items' in newItem && isDropdownItem(newItem)) { + newItem.items = newItem.items.map(getItem) as NavigationLinkItem[]; + } + + if (!(iconSizeKey in newItem) && !isButtonItem(newItem)) { + newItem.iconSize = iconSize; + } + return newItem; + }; + + return getItem; +}