diff --git a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-chromium-linux.png b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-chromium-linux.png index 7b962286..04253e74 100644 Binary files a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-chromium-linux.png and b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-chromium-linux.png differ diff --git a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-webkit-linux.png b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-webkit-linux.png index c0b9c8e2..40308ccb 100644 Binary files a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-webkit-linux.png and b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-dark-webkit-linux.png differ diff --git a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-chromium-linux.png b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-chromium-linux.png index b5de9966..f2063e04 100644 Binary files a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-chromium-linux.png and b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-chromium-linux.png differ diff --git a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-webkit-linux.png b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-webkit-linux.png index 2e7a7b6c..a10322ff 100644 Binary files a/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-webkit-linux.png and b/src/components/AsideHeader/__snapshots__/AsideHeader.visual.test.tsx-snapshots/AsideHeader-render-story-HeaderAlert-light-webkit-linux.png differ diff --git a/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx b/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx index b3c43a4d..5e39e61a 100644 --- a/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx +++ b/src/components/AsideHeader/__stories__/AsideHeader.stories.tsx @@ -9,7 +9,7 @@ import {PageLayout} from '../components/PageLayout/PageLayout'; import {PageLayoutAside} from '../components/PageLayout/PageLayoutAside'; import {AsideHeaderShowcase} from './AsideHeaderShowcase'; -import {DEFAULT_LOGO, menuItemsClamped, menuItemsShowcase} from './moc'; +import {DEFAULT_LOGO, menuItemsClamped, menuItemsMany, menuItemsShowcase} from './moc'; import logoIcon from '../../../../.storybook/assets/logo.svg'; @@ -237,3 +237,48 @@ export const CollapseButtonWrapper = CollapseButtonWrapperTemplate.bind({}); CollapseButtonWrapper.args = { initialCompact: false, }; + +const ManyItemsTemplate: StoryFn = (args) => { + const [compact, setCompact] = React.useState(args.initialCompact); + + return ( + + + + +
+ +
+ + Scroll demonstration with many navigation items + + + Total items: {menuItemsMany?.length || 0}. On low screens, items scroll + with invisible scrollbar instead of collapsing into "..." + +
+
+
+
+ ); +}; + +export const ManyItems = ManyItemsTemplate.bind({}); +ManyItems.args = { + initialCompact: false, +}; +ManyItems.parameters = { + docs: { + description: { + story: + 'Demonstration of scroll functionality with many navigation items. ' + + 'On low screens, all items remain accessible through scrolling with invisible scrollbar.', + }, + }, +}; diff --git a/src/components/AsideHeader/__stories__/moc.tsx b/src/components/AsideHeader/__stories__/moc.tsx index 351ca64c..32f65abd 100644 --- a/src/components/AsideHeader/__stories__/moc.tsx +++ b/src/components/AsideHeader/__stories__/moc.tsx @@ -158,3 +158,68 @@ export const menuItemsClamped = MENU_ITEMS_CLAMPED.concat({ rightAdornment: renderTag('new'), })), ); + +export const generateManyMenuItems = (count = 20): AsideHeaderProps['menuItems'] => { + const items: AsideHeaderProps['menuItems'] = []; + + const sections = [ + 'Dashboard', + 'Analytics', + 'Reports', + 'Settings', + 'Users', + 'Projects', + 'Tasks', + 'Calendar', + 'Messages', + 'Files', + 'Help', + 'Support', + ]; + + sections.forEach((section, index) => { + items.push({ + id: `section-${index}`, + title: section, + icon: Gear, + current: index === 0, + onItemClick(_item) {}, + }); + + if (index % 3 === 0) { + const subItems = [`${section} Overview`, `${section} Details`, `${section} Settings`]; + + subItems.forEach((subItem, subIndex) => { + items.push({ + id: `section-${index}-sub-${subIndex}`, + title: subItem, + icon: Gear, + onItemClick(_item) {}, + }); + }); + } + + if ((index + 1) % 4 === 0 && index < sections.length - 1) { + items.push({ + id: `divider-${index}`, + title: '-', + type: 'divider', + }); + } + }); + + const additionalCount = Math.max(0, count - items.length); + for (let i = 0; i < additionalCount; i++) { + items.push({ + id: `additional-${i}`, + title: `Additional Item ${i + 1}`, + icon: Gear, + rightAdornment: i % 5 === 0 ? renderTag('New') : undefined, + onItemClick(_item) {}, + }); + } + + return items; +}; + +export const menuItemsMany = generateManyMenuItems(25); diff --git a/src/components/AsideHeader/components/CompositeBar/CompositeBar.scss b/src/components/AsideHeader/components/CompositeBar/CompositeBar.scss index 796db16a..117cbefe 100644 --- a/src/components/AsideHeader/components/CompositeBar/CompositeBar.scss +++ b/src/components/AsideHeader/components/CompositeBar/CompositeBar.scss @@ -5,11 +5,24 @@ $block: '.#{variables.$ns}composite-bar'; #{$block} { $class: &; - flex: 1 0 auto; + flex: 1 1 auto; width: 100%; min-height: 40px; + display: flex; + flex-direction: column; & &__root-menu-item[class] { background-color: transparent; } + + &_scrollable { + flex: 1 1 auto; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; + + &::-webkit-scrollbar { + display: none; + } + } } diff --git a/src/components/AsideHeader/components/CompositeBar/CompositeBar.tsx b/src/components/AsideHeader/components/CompositeBar/CompositeBar.tsx index 6f68ef12..ab85e1d5 100644 --- a/src/components/AsideHeader/components/CompositeBar/CompositeBar.tsx +++ b/src/components/AsideHeader/components/CompositeBar/CompositeBar.tsx @@ -1,7 +1,6 @@ import React, {FC, ReactNode, useCallback, useContext, useRef} from 'react'; import {List} from '@gravity-ui/uikit'; -import AutoSizer, {Size} from 'react-virtualized-auto-sizer'; import {ASIDE_HEADER_COMPACT_WIDTH} from '../../../constants'; import {block} from '../../../utils/cn'; @@ -11,12 +10,10 @@ import {Item, ItemProps} from './Item/Item'; import {MultipleTooltip, MultipleTooltipContext, MultipleTooltipProvider} from './MultipleTooltip'; import {COLLAPSE_ITEM_ID} from './constants'; import { - getAutosizeListItems, getItemHeight, getItemsHeight, - getItemsMinHeight, - getMoreButtonItem, getSelectedItemIndex, + sortItemsByAfterMoreButton, } from './utils'; import './CompositeBar.scss'; @@ -39,7 +36,6 @@ export type CompositeBarProps = { }; type CompositeBarViewProps = CompositeBarProps & { - collapseItems?: AsideHeaderItem[]; compositeId?: string; }; @@ -48,7 +44,6 @@ const CompositeBarView: FC = ({ items, onItemClick, onMoreClick, - collapseItems, multipleTooltip = false, compact, compositeId, @@ -220,7 +215,6 @@ const CompositeBarView: FC = ({ onMouseEnter={onMouseEnterByIndex(itemIndex)} onMouseLeave={onMouseLeave} onItemClick={onItemClickByIndex(itemIndex, item.onItemClick)} - collapseItems={collapseItems} /> )} /> @@ -240,7 +234,6 @@ const CompositeBarView: FC = ({ export const CompositeBar: FC = ({ type, items, - menuMoreTitle, onItemClick, onMoreClick, multipleTooltip = false, @@ -252,39 +245,20 @@ export const CompositeBar: FC = ({ } let node: ReactNode; + const sortedItems = sortItemsByAfterMoreButton(items); + if (type === 'menu') { - const minHeight = getItemsMinHeight(items); - const collapseItem = getMoreButtonItem(menuMoreTitle); node = ( -
- {items.length !== 0 && ( - - {(size: Size) => { - const width = Number.isNaN(size.width) ? 0 : size.width; - const height = Number.isNaN(size.height) ? 0 : size.height; - - const {listItems, collapseItems} = getAutosizeListItems( - items, - height, - collapseItem, - ); - return ( -
- -
- ); - }} -
- )} +
+
); } else { @@ -293,7 +267,7 @@ export const CompositeBar: FC = ({
diff --git a/src/components/AsideHeader/components/CompositeBar/Item/Item.tsx b/src/components/AsideHeader/components/CompositeBar/Item/Item.tsx index 5a4cb351..e64f714a 100644 --- a/src/components/AsideHeader/components/CompositeBar/Item/Item.tsx +++ b/src/components/AsideHeader/components/CompositeBar/Item/Item.tsx @@ -1,21 +1,14 @@ import React from 'react'; -import {ActionTooltip, Icon, List, Popup, PopupPlacement, PopupProps} from '@gravity-ui/uikit'; +import {ActionTooltip, Icon, Popup, PopupPlacement, PopupProps} from '@gravity-ui/uikit'; import {AsideHeaderItem} from 'src/components/AsideHeader/types'; import {ASIDE_HEADER_ICON_SIZE} from '../../../../constants'; import {MakeItemParams} from '../../../../types'; import {block} from '../../../../utils/cn'; -import {useAsideHeaderContext} from '../../../AsideHeaderContext'; import {HighlightedItem} from '../HighlightedItem/HighlightedItem'; -import { - COLLAPSE_ITEM_ID, - ITEM_TYPE_REGULAR, - POPUP_ITEM_HEIGHT, - POPUP_PLACEMENT, -} from '../constants'; -import {getSelectedItemIndex} from '../utils'; +import {ITEM_TYPE_REGULAR} from '../constants'; import './Item.scss'; @@ -25,7 +18,6 @@ export interface ItemProps extends AsideHeaderItem {} interface ItemInnerProps extends ItemProps { className?: string; - collapseItems?: AsideHeaderItem[]; onMouseEnter?: () => void; onMouseLeave?: () => void; } @@ -51,7 +43,6 @@ export const defaultPopupOffset: NonNullable = {mainAxis: export const Item: React.FC = (props) => { const { className, - collapseItems, compact, onMouseLeave, onMouseEnter, @@ -73,8 +64,6 @@ export const Item: React.FC = (props) => { qa, } = props; - const [open, toggleOpen] = React.useState(false); - const ref = React.useRef(null); const anchorRef = anchoreRefProp?.current ? anchoreRefProp : ref; const highlightedRef = React.useRef(null); @@ -85,7 +74,6 @@ export const Item: React.FC = (props) => { const icon = props.icon; const iconSize = props.iconSize || ASIDE_HEADER_ICON_SIZE; const iconQa = props.iconQa; - const collapsedItem = props.id === COLLAPSE_ITEM_ID; const handleOpenChangePopup = React.useCallback>( (newOpen, event, reason) => { @@ -110,7 +98,7 @@ export const Item: React.FC = (props) => { @@ -138,16 +126,7 @@ export const Item: React.FC = (props) => { ref={ref} data-qa={qa} onClick={(event: React.MouseEvent) => { - if (collapsedItem) { - /** - * This is the "more" button (three dots) that shows additional menu items in a popup. - * We call onItemClick with collapsed=true to indicate this is a collapse action. - */ - toggleOpen(!open); - onItemClick?.(props, true, event); - } else { - onItemClick?.(props, false, event); - } + onItemClick?.(props, false, event); }} onClickCapture={onItemClickCapture} onMouseEnter={() => { @@ -228,93 +207,8 @@ export const Item: React.FC = (props) => { /> )} {node} - {open && collapsedItem && collapseItems?.length && Boolean(anchorRef?.current) && ( - toggleOpen(false)} - /> - )} ); }; Item.displayName = 'Item'; - -interface CollapsedPopupProps { - anchorRef: React.RefObject; - onOpenChange: () => void; -} - -function CollapsedPopup({ - onItemClick, - collapseItems, - anchorRef, - onOpenChange, -}: ItemInnerProps & CollapsedPopupProps) { - const {compact} = useAsideHeaderContext(); - return collapseItems?.length ? ( - -
- { - const makeCollapseNode = ({ - title: titleEl, - icon: iconEl, - }: MakeItemParams) => { - const [Tag, tagProps] = item.href - ? ['a' as const, {href: item.href}] - : ['button' as const, {}]; - - return ( - ) => { - onItemClick?.(item, true, event); - }} - > - {iconEl} - {titleEl} - - ); - }; - - const titleNode = renderItemTitle(item); - const iconNode = item.icon && ( - - ); - - const params = {title: titleNode, icon: iconNode}; - const opts = { - compact: Boolean(compact), - collapsed: true, - item, - ref: anchorRef, - }; - if (typeof item.itemWrapper === 'function') { - return item.itemWrapper(params, makeCollapseNode, opts); - } else { - return makeCollapseNode(params); - } - }} - /> -
-
- ) : null; -} diff --git a/src/components/AsideHeader/components/CompositeBar/utils.ts b/src/components/AsideHeader/components/CompositeBar/utils.ts index 24e9040e..0b7bc8af 100644 --- a/src/components/AsideHeader/components/CompositeBar/utils.ts +++ b/src/components/AsideHeader/components/CompositeBar/utils.ts @@ -1,10 +1,6 @@ -import {Ellipsis} from '@gravity-ui/icons'; - import {ITEM_HEIGHT} from '../../../constants'; import {AsideHeaderItem} from '../../types'; -import {COLLAPSE_ITEM_ID} from './constants'; - export function getItemHeight(compositeItem: AsideHeaderItem) { switch (compositeItem.type) { case 'action': @@ -26,94 +22,9 @@ export function getSelectedItemIndex(compositeItems: AsideHeaderItem[]) { return index === -1 ? undefined : index; } -export function getPinnedItems(compositeItems: AsideHeaderItem[]) { - const pinnedItems: AsideHeaderItem[] = []; - for (const compositeItem of compositeItems) { - if (compositeItem.pinned) { - pinnedItems.push(compositeItem); - } else if (compositeItem.type === 'divider') { - if (pinnedItems.length > 0 && pinnedItems[pinnedItems.length - 1].type !== 'divider') { - pinnedItems.push(compositeItem); - } - } - } - return pinnedItems; -} - -export function getItemsMinHeight(compositeItems: AsideHeaderItem[]) { - const pinnedItems = getPinnedItems(compositeItems); - const afterMoreButtonItems = compositeItems.filter(({afterMoreButton}) => afterMoreButton); - - return ( - getItemsHeight(pinnedItems) + - getItemsHeight(afterMoreButtonItems) + - (pinnedItems.length === compositeItems.length ? 0 : ITEM_HEIGHT) - ); -} - -export function getMoreButtonItem(menuMoreTitle?: string): AsideHeaderItem { - return { - id: COLLAPSE_ITEM_ID, - title: menuMoreTitle, - icon: Ellipsis, - iconSize: 18, - }; -} - -export function getAutosizeListItems( - compositeItems: AsideHeaderItem[], - height: number, - collapseItem: AsideHeaderItem, -): { - listItems: AsideHeaderItem[]; - collapseItems: AsideHeaderItem[]; -} { +export function sortItemsByAfterMoreButton(compositeItems: AsideHeaderItem[]): AsideHeaderItem[] { const afterMoreButtonItems = compositeItems.filter(({afterMoreButton}) => afterMoreButton); const regularItems = compositeItems.filter(({afterMoreButton}) => !afterMoreButton); - const listItems = [...regularItems, ...afterMoreButtonItems]; - - const allItemsHeight = getItemsHeight(listItems); - if (allItemsHeight <= height) { - return {listItems, collapseItems: []}; - } - - const collapseItemHeight = getItemHeight(collapseItem); - - listItems.splice(regularItems.length, 0, collapseItem); - const collapseItems: AsideHeaderItem[] = []; - - let listHeight = allItemsHeight + collapseItemHeight; - let index = listItems.length; - while (listHeight > height) { - if (index === 0) { - break; - } - index--; - - const compositeItem = listItems[index]; - if ( - compositeItem.pinned || - compositeItem.id === COLLAPSE_ITEM_ID || - compositeItem.afterMoreButton - ) { - continue; - } - if (compositeItem.type === 'divider') { - if (index + 1 < listItems.length && listItems[index + 1]?.type === 'divider') { - listHeight -= getItemHeight(compositeItem); - listItems.splice(index, 1); - } - continue; - } - listHeight -= getItemHeight(compositeItem); - collapseItems.unshift(...listItems.splice(index, 1)); - } - if ( - listItems[index]?.type === 'divider' && - (index === 0 || listItems[index - 1]?.type === 'divider') - ) { - listItems.splice(index, 1); - } - return {listItems, collapseItems}; + return [...regularItems, ...afterMoreButtonItems]; }