diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 9f54a624..1c5d772d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -5,6 +5,7 @@ import {withMobile} from './decorators/withMobile'; import {withLang} from './decorators/withLang'; import './styles.scss'; + import '@gravity-ui/uikit/styles/styles.css'; uiKitConfigure({ diff --git a/src/components/AllPagesPanel/useGroupedMenuItems.ts b/src/components/AllPagesPanel/useGroupedMenuItems.ts deleted file mode 100644 index 73db9c6f..00000000 --- a/src/components/AllPagesPanel/useGroupedMenuItems.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {useMemo} from 'react'; - -import {MenuItem} from '../types'; - -import {ALL_PAGES_ID} from './constants'; -import i18n from './i18n'; - -export const useGroupedMenuItems = (items: MenuItem[]) => { - const allPagesMenuItems = useMemo(() => { - const filteredItems = items.filter( - (item) => item.type !== 'divider' && item.id !== ALL_PAGES_ID, - ); - filteredItems.sort((a, b) => { - if (a.type === 'action') { - return 1; - } - if (b.type === 'action') { - return -1; - } - return 0; - }); - const groupedItems = filteredItems.reduce( - (acc, item) => { - const category = item.category || i18n('all-panel.menu.category.allOther'); - if (!acc[category]) { - acc[category] = []; - } - acc[category].push(item); - return acc; - }, - {} as {[key: string]: MenuItem[]}, - ); - return groupedItems; - }, [items]); - - return allPagesMenuItems; -}; diff --git a/src/components/AllPagesPanel/useVisibleMenuItems.ts b/src/components/AllPagesPanel/useVisibleMenuItems.ts deleted file mode 100644 index a3d76770..00000000 --- a/src/components/AllPagesPanel/useVisibleMenuItems.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {useMemo} from 'react'; - -import {useAsideHeaderInnerContext} from '../AsideHeader/AsideHeaderContext'; -import {MenuItem} from '../types'; - -export const useVisibleMenuItems = (): MenuItem[] => { - const {menuItems, allPagesIsAvailable} = useAsideHeaderInnerContext(); - return useMemo(() => { - if (!allPagesIsAvailable) { - return menuItems; - } - let lastVisibleIndex = 0; - return menuItems.filter((item: MenuItem, index: number, items: MenuItem[]): boolean => { - if (item.hidden) { - return false; - } - - if ( - index > 0 && - item.type === 'divider' && - (items[lastVisibleIndex].type === 'divider' || items[lastVisibleIndex].hidden) - ) { - return false; - } - lastVisibleIndex = index; - return true; - }); - }, [allPagesIsAvailable, menuItems]); -}; diff --git a/src/components/AsideHeader/AsideHeaderContext.ts b/src/components/AsideHeader/AsideHeaderContext.ts index 3b09beff..0c4add0d 100644 --- a/src/components/AsideHeader/AsideHeaderContext.ts +++ b/src/components/AsideHeader/AsideHeaderContext.ts @@ -1,15 +1,13 @@ import React from 'react'; -import {MenuItem} from '../types'; - -import {AsideHeaderInnerProps} from './types'; +import {AsideHeaderInnerProps, AsideHeaderItem} from './types'; export interface AsideHeaderInnerContextType extends AsideHeaderInnerProps { - menuItems: MenuItem[]; - defaultMenuItems?: MenuItem[]; + menuItems: AsideHeaderItem[]; + defaultMenuItems?: AsideHeaderItem[]; allPagesIsAvailable: boolean; onItemClick: ( - item: MenuItem, + item: AsideHeaderItem, collapsed: boolean, event: React.MouseEvent, ) => void; diff --git a/src/components/AsideHeader/README-ru.md b/src/components/AsideHeader/README-ru.md index 1d3c3a0d..aac0c33d 100644 --- a/src/components/AsideHeader/README-ru.md +++ b/src/components/AsideHeader/README-ru.md @@ -41,8 +41,6 @@ import {AsideHeader} from '@gravity-ui/navigation'; Данный блок, как правило, включает логотип и другие элементы, расположенные под ним, которые присутствуют на всех страницах сайта. Для быстрого перехода на главную страницу можно использовать кликабельный логотип. При необходимости под логотипом можно разместить другие элементы, такие как поисковая строка и каталог. -Элементы данного блока поддерживают тултипы, всплывающие окна и выдвижные боковые панели — для их применения необходимо задать соответствующие настройки. - ### Средний блок (`menuItems`) Данный блок является основным, а его содержимое может меняться в зависимости от текущей страницы. Один из примеров использования — навигация по многостраничным сайтам. @@ -56,17 +54,23 @@ import {AsideHeader} from '@gravity-ui/navigation'; **Примечание**: пользователь управляет списком элементов меню, полученным из обратного вызова, и передает новое состояние элементов в `AsideHeader`. +Элементы данного блока могут иметь множественную всплывающую подсказку. + ### Нижний блок Нижний блок (футер) повышает удобство пользователей, обеспечивая легкий доступ к элементам и вспомогательным ресурсам. Он позволяет связаться со службой поддержки и включает дополнительную информацию, чтобы пользователю было проще ориентироваться. В футере можно использовать как собственные компоненты, так и `FooterItem`. +### Элементы + +Элементы блока поддерживают тултипы, всплывающие окна и выдвижные боковые панели — для их применения необходимо задать соответствующие настройки. + #### Выделение элемента Выделение элемента поверх модальных окон может быть полезным, если пользователь хочет отправить сообщение об ошибке через форму обратной связи, открываемую в модальном окне. -В компоненте `FooterItem` можно передать свойство `bringForward`, которое отображает иконку поверх модальных окон. Кроме того, в `AsideHeader` необходимо передать функцию, которая будет уведомлять об открытии модальных окон. +В компоненте `FooterItem` и в конфигурации элементов `menuItems`, `subheaderItems` можно передать свойство `bringForward`, которое отображает иконку поверх модальных окон. Кроме того, в `AsideHeader` необходимо передать функцию, которая будет уведомлять об открытии модальных окон. ## Рендеринг контента @@ -130,12 +134,12 @@ export const Aside: FC = () => { | headerDecoration | Цвет фона верхнего блока с элементами логотипа и подзаголовка. | `boolean` | `false` | | hideCollapseButton | Скрывает `CollapseButton`. Для установки дефолтного состояния элемента навигации используйте свойство `compact`. | `boolean` | `false` | | logo | Контейнер логотипа, включающий иконку с заголовком и обрабатывающий клики. | [`Logo`](./../Logo/Readme.md#logo) | | -| menuItems | Элементы в среднем блоке навигации. | `Array` | `[]` | +| menuItems | Элементы в среднем блоке навигации. | `Array` | `[]` | | menuMoreTitle | Дополнительный заголовок для `menuItems`, если элементы не помещаются. | `string` | `"Ещё"` `"More"` | | multipleTooltip | Отображает несколько тултипов при наведении на элементы меню (`menuItems`) в свернутом состоянии. | `boolean` | `false` | | onChangeCompact | Обратный вызов, срабатывающий при изменении визуального состояния элемента навигации. | `(compact: boolean) => void;` | | | onClosePanel | Обратный вызов, срабатывающий при закрытии панели. Панели можно добавлять через свойство `PanelItems`. | `() => void;` | | -| onMenuItemsChanged | Обратный вызов, срабатывающий при изменении списка `menuItems` в `AllPagesPanel`. | `(items: Array) => void` | | +| onMenuItemsChanged | Обратный вызов, срабатывающий при изменении списка `menuItems` в `AllPagesPanel`. | `(items: Array) => void` | | | onMenuMoreClick | Обратный вызов, срабатывающий при нажатии кнопки **More** («Еще»), если часть элементов скрыта. | `() => void;` | | | onAllPagesClick | Обратный вызов, срабатывающий при нажатии кнопки **All pages** («Все станицы»). | `() => void;` | | | openModalSubscriber | Функция для уведомления `AsideHeader` об изменении состояния видимости модальных окон. | `( (open: boolean) => void) => void` | | @@ -143,33 +147,44 @@ export const Aside: FC = () => { | renderContent | Функция рендеринга основного контента справа от `AsideHeader`. | `(data: {size: number}) => React.ReactNode` | | | renderFooter | Функция рендеринга нижнего блока навигации. | `(data: {size: number}) => React.ReactNode` | | | ref | Ссылка на якорь целевого всплывающего окна. | `React.ForwardedRef` | | -| subheaderItems | Элементы, расположенные под логотипом в верхнем блоке навигации. | ` Array<{item: MenuItem; enableTooltip?: boolean; bringForward?: boolean}>` | `[]` | +| subheaderItems | Элементы, расположенные под логотипом в верхнем блоке навигации. | ` Array` | `[]` | | topAlert | Контейнер над элементом навигации на основе компонента `Alert` из фреймворка UIKit. | `TopAlert` | | | qa | Значение, которое будет передано в атрибут `data-qa` контейнера `AsideHeader`. | `string` | | -### `MenuItem` - -| Имя | Описание | Тип | Значение по умолчанию | -| :----------------- | :------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------: | -| afterMoreButton | Элемент будет всегда отображаться в конце списка, даже если не помещается. | `boolean` | | -| category | Категория, к которой относится элемент меню. Используется для группировки в режиме отображения или редактирования всех страниц. | `string` | `"Остальное"` `"All other"` | -| current | Текущий (выбранный) элемент. | `boolean` | `false` | -| hidden | Видимость элемента в меню. | `boolean` | `false` | -| icon | Иконка меню на основе компонента `Icon` из фреймворка UIKit. | [`IconProps['data']`](https://github.com/gravity-ui/uikit/tree/main/src/components/Icon#properties) | | -| iconSize | Размер иконки меню. | `number` `string` | `18` | -| iconQa | Значение, которое будет передано в атрибут `data-qa` контейнера `Icon`. | `string` | | -| id | Идентификатор элемента меню. | `string` | | -| itemWrapper | Обертка элемента меню. | [`ItemWrapper`](https://github.com/gravity-ui/navigation/blob/b8367cf343fc20304bc3c8d9a337d9f7d803a9b3/src/components/types.ts#L32-L41) | | -| link | HTML-атрибут `href`. | `string` | | -| onItemClick | Обратный вызов, срабатывающий при клике по элементу. | `(item: MenuItem, collapsed: boolean, event: React.MouseEvent) => void` | | -| onItemClickCapture | Обратный вызов, срабатывающий при клике по элементу. | ` (event: React.SyntheticEvent) => void` | | -| order | Определяет порядок отображения в элементе навигации. | `number` | | -| pinned | Запрещает скрытие элемента меню в `AllPagesPanel`. | `boolean` | `false` | -| rightAdornment | Настраивает правую часть элемента меню. | `React.ReactNode` | | -| title | Заголовок элемента меню. | `React.ReactNode` | | -| tooltipText | Содержимое тултипа. | `React.ReactNode` | | -| type | Тип элемента меню, определяющий его внешний вид: `"regular"`, `"action"` или `"divider"`. | `string` | `"regular"` | -| qa | Значение, которое будет передано в атрибут `data-qa`. | `string` | | +### `AsideHeaderItem` + +| Имя | Описание | Тип | Значение по умолчанию | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------: | +| afterMoreButton | Элемент будет всегда отображаться в конце списка, даже если не помещается. | `boolean` | | +| category | Категория, к которой относится элемент меню. Используется для группировки в режиме отображения или редактирования всех страниц. | `string` | `"Остальное"` `"All other"` | +| current | Текущий (выбранный) элемент. | `boolean` | `false` | +| hidden | Видимость элемента в меню. | `boolean` | `false` | +| icon | Иконка меню на основе компонента `Icon` из фреймворка UIKit. | [`IconProps['data']`](https://github.com/gravity-ui/uikit/tree/main/src/components/Icon#properties) | | +| iconSize | Размер иконки меню. | `number` `string` | `18` | +| iconQa | Значение, которое будет передано в атрибут `data-qa` контейнера `Icon`. | `string` | | +| id | Идентификатор элемента меню. | `string` | | +| itemWrapper | Обертка элемента меню. | [`ItemWrapper`](https://github.com/gravity-ui/navigation/blob/b8367cf343fc20304bc3c8d9a337d9f7d803a9b3/src/components/types.ts#L32-L41) | | +| link | HTML-атрибут `href`. | `string` | | +| onItemClick | Обратный вызов, срабатывающий при клике по элементу. | `(item: MenuItem, collapsed: boolean, event: React.MouseEvent) => void` | | +| onItemClickCapture | Обратный вызов, срабатывающий при клике по элементу. | ` (event: React.SyntheticEvent) => void` | | +| order | Определяет порядок отображения в элементе навигации. | `number` | | +| pinned | Запрещает скрытие элемента меню в `AllPagesPanel`. | `boolean` | `false` | +| rightAdornment | Настраивает правую часть элемента меню. | `React.ReactNode` | | +| title | Заголовок элемента меню. | `React.ReactNode` | | +| tooltipText | Содержимое тултипа. | `React.ReactNode` | | +| type | Тип элемента меню, определяющий его внешний вид: `"regular"`, `"action"` или `"divider"`. | `string` | `"regular"` | +| qa | Значение, которое будет передано в атрибут `data-qa`. | `string` | | +| enableTooltip | Отображать ли подсказку. | `boolean \| undefined` | `true` | +| onCollapseItemClick | Обратный вызов, срабатывающий при клике на свёрнутый элемент. | ` () => void \| undefined` | | +| bringForward | Отображать ли иконку поверх модальных окон. | `boolean \| undefined` | | +| compact | Флаг, отвечающий за отображение элемента меню в компактном формате. | `boolean \| undefined` | | +| popupVisible | Флаг, отвечающий за отображение всплывающего окна. | `boolean \| undefined` | `false` | +| popupAnchorElement | Компонент, к которому привязано всплывающее окно. | [`PopupProps['anchorElement']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L73) | | +| popupPlacement | Расположение всплывающего окна отностилеьно компонента привязки. | [`PopupProps['placement']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L69) | | +| popupOffset | Смещение всплывающего окна относительно компонента привязки. | [`PopupProps['offset']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L71) | `{mainAxis: 8, crossAxis: -20}` | +| popupKeepMounted | Всплывающее окно не будет удалено из DOM при скрытии. | `boolean \| undefined` | `false` | +| renderPopupContent | Функция отвечает за отрисовку контента во всплывающем окне. | `(() => React.ReactNode) \| undefined` | | +| onOpenChangePopup | Обратный вызов для изменения состояния popupVisible, например, при отклонении. | [`PopupProps['onOpenChange']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L61) | | ### `TopAlert` diff --git a/src/components/AsideHeader/README.md b/src/components/AsideHeader/README.md index 21de1727..25c7ba58 100644 --- a/src/components/AsideHeader/README.md +++ b/src/components/AsideHeader/README.md @@ -41,8 +41,6 @@ Navigation includes 3 parts: the top, the middle and the bottom. These sections The section usually contains general elements for all site pages and includes the logo and the elements below it. Clickable logo can be useful for a quick navigation to the home page, if necessary the element (e.g. search, catalogue) is placed under it. -The elements have access to tooltip, popup, drawers, it is enough to select the desired behavior when configuring this section. - ### The Middle (menuItems) The main section usually depends on context of the page — one of examples using navigation on the multipage sites. @@ -56,17 +54,23 @@ The `onMenuItemsChanged` callback is required for adding extra component `All Pa **Important note**: A user manages a modified list of the menu items that they receive from the callback and provides the new state of items to `AsideHeader`. +The elements of this block can have multiple tooltips. + ### The Bottom The Footer improves user experience by offering easy access to the elements and supplementary resources. It gives opportunity to connect with support add custom information to be sure that user will not get lost. There can be both their own components inside, or also you can use `FooterItem`. +## Elements + +The elements have access to tooltip, popup, drawers, it is enough to select the desired behavior when configuring this section. + #### Highlighting element Highlighting an element over modal windows can be useful when a user wants to report an error via a feedback form, and the form with bug is opened in a modal window. -In the `FooterItem` component, you can pass a `bringForward` prop, which renders the icon above modal windows. Additionally, you need to pass a function to `AsideHeader` that will notify about the opening of modal windows. +In the `FooterItem` component and in the configuration of the `menuItems` and `subheaderItems` elements, you can pass the `bringForward` property, which displays an icon on top of the modal windows. Additionally, in the `AsideHeader`, you need to pass a function that will notify you when the modal windows are opened. ## Rendering Content @@ -131,12 +135,12 @@ export const Aside: FC = () => { | headerDecoration | Color background of the top section with logo and subheader items | `boolean` | `false` | | hideCollapseButton | Hiding `CollapseButton`. Use `compact` prop for setting default navigation state | `boolean` | `false` | | logo | Logo container includes icon, title, handling clicks | [`Logo`](https://github.com/gravity-ui/navigation/blob/main/src/components/Logo/Readme.md#logo) | | -| menuItems | Items in the navigation middle section | `Array` | `[]` | +| menuItems | Items in the navigation middle section | `Array` | `[]` | | menuMoreTitle | Additional element title of menuItems if elements don't fit | `string` | `"Ещё"` `"More"` | | multipleTooltip | Show the multiple tooltip by hovering elements of menuItems in collapsed state | `boolean` | `false` | | onChangeCompact | Callback will be called when changing navigation visual state | `(compact: boolean) => void;` | | | onClosePanel | Callback will be called when closing panel. You can add panels via `PanelItems` prop | `() => void;` | | -| onMenuItemsChanged | Callback will be called when updating list of the menuItems in `AllPagesPanel` | `(items: Array) => void` | | +| onMenuItemsChanged | Callback will be called when updating list of the menuItems in `AllPagesPanel` | `(items: Array) => void` | | | onMenuMoreClick | Callback will be called when some items don't fit and "more" button is clicked | `() => void;` | | | onAllPagesClick | Callback will be called when "All pages" button is clicked | `() => void;` | | | openModalSubscriber | Function notifies `AsideHeader` about Modals visibility changes | `( (open: boolean) => void) => void` | | @@ -144,33 +148,44 @@ export const Aside: FC = () => { | renderContent | Function rendering the main content at the right of the `AsideHeader` | `(data: {size: number}) => React.ReactNode` | | | renderFooter | Function rendering the navigation bottom section | `(data: {size: number}) => React.ReactNode` | | | ref | `ref` to target popup anchor | `React.ForwardedRef` | | -| subheaderItems | Items in the navigation top section under Logo | `Array<{item: MenuItem; enableTooltip?: boolean; bringForward?: boolean}>` | `[]` | +| subheaderItems | Items in the navigation top section under Logo | `Array` | `[]` | | topAlert | The container above the navigation based on the uikit `Alert` component | `TopAlert` | | | qa | The value to be passed to `data-qa` attribute of the `AsideHeader` container | `string` | | -### `MenuItem` - -| Name | Description | Type | Default | -| :----------------- | :------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------: | -| afterMoreButton | The menu item will be placed in the end, even item don't fit | `boolean` | | -| category | The category to which the menu item belongs. Need for grouping in the display/editing mode of all pages | `string` | `"Остальное"` `"All other"` | -| current | The current/selected item | `boolean` | `false` | -| hidden | Visibility item in the menu | `boolean` | `false` | -| icon | Menu icon based on the uikit `Icon` component | [`IconProps['data']`](https://github.com/gravity-ui/uikit/tree/main/src/components/Icon#properties) | | -| iconSize | Menu icon size | `number` `string` | `18` | -| iconQa | The value to be passed to `data-qa` attribute of the `Icon` container | `string` | | -| id | The menu item id | `string` | | -| itemWrapper | The menu item wrapper | [`ItemWrapper`](https://github.com/gravity-ui/navigation/blob/b8367cf343fc20304bc3c8d9a337d9f7d803a9b3/src/components/types.ts#L32-L41) | | -| link | HTML href attribute | `string` | | -| onItemClick | Callback will be called when clicking on the item | `(item: MenuItem, collapsed: boolean, event: React.MouseEvent) => void` | | -| onItemClickCapture | Callback will be called when clicking on the item | ` (event: React.SyntheticEvent) => void` | | -| order | Determine the display order in the navigation | `number` | | -| pinned | The parameter restricts hiding menu item in the `AllPagesPanel` | `boolean` | `false` | -| rightAdornment | Customize right side of the menu item | `React.ReactNode` | | -| title | The menu item title | `React.ReactNode` | | -| tooltipText | Tooltip content | `React.ReactNode` | | -| type | The menu item type changes appearance: `"regular"`, `"action"`, `"divider"` | `string` | `"regular"` | -| qa | The value to be passed to `data-qa` attribute | `string` | | +### `AsideHeaderItem` + +| Name | Description | Type | Default | +| :------------------ | :------------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------: | +| afterMoreButton | The menu item will be placed in the end, even item don't fit | `boolean` | | +| category | The category to which the menu item belongs. Need for grouping in the display/editing mode of all pages | `string` | `"Остальное"` `"All other"` | +| current | The current/selected item | `boolean` | `false` | +| hidden | Visibility item in the menu | `boolean` | `false` | +| icon | Menu icon based on the uikit `Icon` component | [`IconProps['data']`](https://github.com/gravity-ui/uikit/tree/main/src/components/Icon#properties) | | +| iconSize | Menu icon size | `number` `string` | `18` | +| iconQa | The value to be passed to `data-qa` attribute of the `Icon` container | `string` | | +| id | The menu item id | `string` | | +| itemWrapper | The menu item wrapper | [`ItemWrapper`](https://github.com/gravity-ui/navigation/blob/b8367cf343fc20304bc3c8d9a337d9f7d803a9b3/src/components/types.ts#L32-L41) | | +| link | HTML href attribute | `string` | | +| onItemClick | Callback will be called when clicking on the item | `(item: AsideHeaderItem, collapsed: boolean, event: React.MouseEvent) => void` | | +| onItemClickCapture | Callback will be called when clicking on the item | ` (event: React.SyntheticEvent) => void` | | +| order | Determine the display order in the navigation | `number` | | +| pinned | The parameter restricts hiding menu item in the `AllPagesPanel` | `boolean` | `false` | +| rightAdornment | Customize right side of the menu item | `React.ReactNode` | | +| title | The menu item title | `React.ReactNode` | | +| tooltipText | Tooltip content | `React.ReactNode` | | +| type | The menu item type changes appearance: `"regular"`, `"action"`, `"divider"` | `string` | `"regular"` | +| qa | The value to be passed to `data-qa` attribute | `string` | | +| enableTooltip | Whether to display a tooltip. | `boolean \| undefined` | `true` | +| onCollapseItemClick | A callback that is triggered when you click on a collapsed element. | ` () => void \| undefined` | | +| bringForward | Whether to display the icon on top of modal windows. | `boolean \| undefined` | | +| compact | The flag responsible for displaying the menu item in a compact form. | `boolean \| undefined` | | +| popupVisible | The flag responsible for displaying the pop-up window. | `boolean \| undefined` | `false` | +| popupAnchorElement | The component to which the pop-up window is attached. | [`PopupProps['anchorElement']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L73) | | +| popupPlacement | The location of the pop-up window relative to the anchor component. | [`PopupProps['placement']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L69) | | +| popupOffset | The offset of the pop-up window relative to the anchor component. | [`PopupProps['offset']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L71) | `{mainAxis: 8, crossAxis: -20}` | +| popupKeepMounted | The pop-up window will not be removed from the DOM when it is opened. | `boolean \| undefined` | `false` | +| renderPopupContent | This function is responsible for rendering content in a pop-up window. | `(() => React.ReactNode) \| undefined` | | +| onOpenChangePopup | A callback for changing the popupVisible state, such as when it is dismissed. | [`PopupProps['onOpenChange']`](https://github.com/gravity-ui/uikit/blob/7748aaeec8dc7414487f7c06c899f16b275b25ef/src/components/Popup/Popup.tsx#L61) | | ### `TopAlert` diff --git a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss index 34ce6de2..6375d8a5 100644 --- a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss +++ b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.scss @@ -1,4 +1,4 @@ -@import '../../../../styles/mixins'; +@use '../../../../styles/mixins'; body { margin: 0; @@ -6,7 +6,7 @@ body { .aside-header-showcase { &__content { - @include text-body-3; + @include mixins.text-body-3; padding: 40px; } @@ -71,7 +71,7 @@ body { } &__item-accent { - @include text-accent; + @include mixins.text-accent; width: 100%; height: 100%; } diff --git a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx index a74ba547..6febace9 100644 --- a/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx +++ b/src/components/AsideHeader/__stories__/AsideHeaderShowcase.tsx @@ -109,20 +109,18 @@ export const AsideHeaderShowcase: React.FC = ({ customBackgroundClassName={customBackgroundClassName} subheaderItems={[ { - item: { - id: 'services', - title: 'Services', - icon: Gear, - onItemClick: () => { - setVisiblePanel(undefined); - setSubheaderPopupVisible(!subheaderPopupVisible); - }, + id: 'services', + title: 'Services', + icon: Gear, + onItemClick: () => { + setVisiblePanel(undefined); + setSubheaderPopupVisible(!subheaderPopupVisible); }, - popupAnchor: ref, + popupRef: ref, popupPlacement: ['right-start'], popupOffset: {mainAxis: 10, crossAxis: 10}, popupVisible: subheaderPopupVisible, - onClosePopup: () => setSubheaderPopupVisible(false), + onOpenChangePopup: () => setSubheaderPopupVisible(false), renderPopupContent: () => { return (
@@ -137,17 +135,15 @@ export const AsideHeaderShowcase: React.FC = ({ }, }, { - item: { - id: 'search', - title: 'Search', - qa: 'subheader-item-search', - icon: Magnifier, - current: visiblePanel === Panel.Search, - onItemClick: () => - setVisiblePanel( - visiblePanel === Panel.Search ? undefined : Panel.Search, - ), - }, + id: 'search', + title: 'Search', + qa: 'subheader-item-search', + icon: Magnifier, + current: visiblePanel === Panel.Search, + onItemClick: () => + setVisiblePanel( + visiblePanel === Panel.Search ? undefined : Panel.Search, + ), }, ]} compact={compact} @@ -159,30 +155,28 @@ export const AsideHeaderShowcase: React.FC = ({ - Minor issue - Now -
- ), - tooltipText: 'Minor issue (Now)', - onItemClick: () => { - setVisiblePanel(undefined); - setPopupVisible(!popupVisible); - }, + id={'infra'} + icon={Gear} + current={popupVisible} + qa={'footer-item-gear'} + iconQa={'footer-item-icon-gear'} + title={ +
+ Minor issue + Now +
+ } + tooltipText={'Minor issue (Now)'} + onItemClick={() => { + setVisiblePanel(undefined); + setPopupVisible(!popupVisible); }} enableTooltip={false} popupVisible={popupVisible} - popupAnchor={asideRef} + popupRef={asideRef} popupPlacement={['right-end']} popupOffset={{mainAxis: 10, crossAxis: 10}} - onClosePopup={() => setPopupVisible(false)} + onOpenChangePopup={() => setPopupVisible(false)} popupKeepMounted={true} renderPopupContent={() => { return ( @@ -198,45 +192,42 @@ export const AsideHeaderShowcase: React.FC = ({ }} /> - Settings with panel - - ), - current: visiblePanel === Panel.ProjectSettings, - itemWrapper: (params, makeItem) => - makeItem({ - ...params, - icon: , - }), - onItemClick: () => { - setVisiblePanel( - visiblePanel === Panel.ProjectSettings - ? undefined - : Panel.ProjectSettings, - ); - }, + id={'project-settings'} + title={'Settings with panel'} + tooltipText={ +
+ Settings with panel +
+ } + current={visiblePanel === Panel.ProjectSettings} + itemWrapper={(params, makeItem) => + makeItem({ + ...params, + icon: , + }) + } + onItemClick={() => { + setVisiblePanel( + visiblePanel === Panel.ProjectSettings + ? undefined + : Panel.ProjectSettings, + ); }} bringForward compact={compact} /> { - setVisiblePanel( - visiblePanel === Panel.UserSettings - ? undefined - : Panel.UserSettings, - ); - }, + id={'user-settings'} + icon={Gear} + title={'User Settings with panel'} + tooltipText={'User Settings with panel'} + current={visiblePanel === Panel.UserSettings} + onItemClick={() => { + setVisiblePanel( + visiblePanel === Panel.UserSettings + ? undefined + : Panel.UserSettings, + ); }} compact={compact} /> diff --git a/src/components/AsideHeader/__stories__/moc.tsx b/src/components/AsideHeader/__stories__/moc.tsx index 6f7a02bf..6a1126fb 100644 --- a/src/components/AsideHeader/__stories__/moc.tsx +++ b/src/components/AsideHeader/__stories__/moc.tsx @@ -123,12 +123,7 @@ const MENU_ITEMS_CLAMPED: AsideHeaderProps['menuItems'] = [ title: MENU_ITEMS_CLAMPED_TITLE, icon: Gear, }, - { - id: 'text-action', - title: MENU_ITEMS_CLAMPED_TITLE, - icon: Gear, - type: 'action', - }, + {id: 'text-action', title: MENU_ITEMS_CLAMPED_TITLE, icon: Gear, type: 'action'}, { id: 'text-link', title: MENU_ITEMS_CLAMPED_TITLE, diff --git a/src/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.scss b/src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.scss similarity index 86% rename from src/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.scss rename to src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.scss index a6565705..4693cfe6 100644 --- a/src/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.scss +++ b/src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.scss @@ -1,5 +1,5 @@ -@use '../../variables'; -@use '../../../../styles/mixins'; +@use '../../../../variables'; +@use '../../../../../../styles/mixins'; .#{variables.$ns}all-pages-list-item { @include mixins.accessibility-button; diff --git a/src/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.tsx b/src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.tsx similarity index 92% rename from src/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.tsx rename to src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.tsx index 333cbd06..f76c0f0c 100644 --- a/src/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.tsx +++ b/src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/AllPagesListItem.tsx @@ -3,15 +3,16 @@ import React, {MouseEvent, useCallback} from 'react'; import {Pin, PinFill} from '@gravity-ui/icons'; import {Button, Icon} from '@gravity-ui/uikit'; -import {MenuItem} from '../../types'; -import {block} from '../../utils/cn'; +import {AsideHeaderItem} from 'src/components/AsideHeader/types'; + +import {block} from '../../../../utils/cn'; import './AllPagesListItem.scss'; const b = block('all-pages-list-item'); interface AllPagesListItemProps { - item: MenuItem; + item: AsideHeaderItem; editMode?: boolean; enableSorting?: boolean; onToggle: () => void; diff --git a/src/components/AllPagesPanel/AllPagesListItem/index.ts b/src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/index.ts similarity index 100% rename from src/components/AllPagesPanel/AllPagesListItem/index.ts rename to src/components/AsideHeader/components/AllPagesPanel/AllPagesListItem/index.ts diff --git a/src/components/AllPagesPanel/AllPagesPanel.scss b/src/components/AsideHeader/components/AllPagesPanel/AllPagesPanel.scss similarity index 95% rename from src/components/AllPagesPanel/AllPagesPanel.scss rename to src/components/AsideHeader/components/AllPagesPanel/AllPagesPanel.scss index 9f73ac90..048c8f4e 100644 --- a/src/components/AllPagesPanel/AllPagesPanel.scss +++ b/src/components/AsideHeader/components/AllPagesPanel/AllPagesPanel.scss @@ -1,4 +1,4 @@ -@use '../variables'; +@use '../../../variables'; .#{variables.$ns}all-pages-panel { min-width: 300px; diff --git a/src/components/AllPagesPanel/AllPagesPanel.tsx b/src/components/AsideHeader/components/AllPagesPanel/AllPagesPanel.tsx similarity index 80% rename from src/components/AllPagesPanel/AllPagesPanel.tsx rename to src/components/AsideHeader/components/AllPagesPanel/AllPagesPanel.tsx index 4d211c49..eafb12e4 100644 --- a/src/components/AllPagesPanel/AllPagesPanel.tsx +++ b/src/components/AsideHeader/components/AllPagesPanel/AllPagesPanel.tsx @@ -1,11 +1,11 @@ import React, {ReactNode, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Gear} from '@gravity-ui/icons'; -import {Button, Flex, Icon, List, ListItemData, Text, Tooltip} from '@gravity-ui/uikit'; +import {Button, Flex, Icon, List, ListItemData, ListProps, Text, Tooltip} from '@gravity-ui/uikit'; -import {useAsideHeaderInnerContext} from '../AsideHeader/AsideHeaderContext'; -import {MenuItem} from '../types'; -import {block} from '../utils/cn'; +import {block} from '../../../utils/cn'; +import {useAsideHeaderInnerContext} from '../../AsideHeaderContext'; +import {AsideHeaderItem} from '../../types'; import {AllPagesListItem} from './AllPagesListItem'; import {ALL_PAGES_ID} from './constants'; @@ -48,17 +48,23 @@ export const AllPagesPanel: React.FC = (props) => { } }, [isEditMode, onEditModeChanged, editMenuProps]); - const onItemClick = useCallback((item: ListItemData) => { - //@ts-ignore TODO fix when @gravity-ui/uikit/List will provide event arg on item click - item.onItemClick?.(item, false); - }, []); + const onItemClick = useCallback['onItemClick']>>( + (item, _index, _forwardKey, event) => { + // TODO: make event an optional argument + item.onItemClick?.(item, false, event as React.MouseEvent); + }, + [], + ); const togglePageVisibility = useCallback( - (item: MenuItem) => { + (item: AsideHeaderItem) => { if (!onMenuItemsChanged) { return; } - const changedItem: MenuItem = {...item, hidden: !item.hidden}; + const changedItem: AsideHeaderItem = { + ...item, + hidden: !item.hidden, + }; const originItems = menuItemsRef.current.filter( (menuItem) => menuItem.id !== ALL_PAGES_ID, @@ -81,18 +87,22 @@ export const AllPagesPanel: React.FC = (props) => { }, [setDraggingItemTitle]); const itemRender = useCallback( - (item: ListItemData, _isActive: boolean, _itemIndex: number) => { + ( + asideHeaderItem: ListItemData, + _isActive: boolean, + _itemIndex: number, + ) => { const onDragStart = () => { - setDraggingItemTitle(item.title); + setDraggingItemTitle(asideHeaderItem.title); }; return ( togglePageVisibility(item)} + onToggle={() => togglePageVisibility(asideHeaderItem)} enableSorting={editMenuProps?.enableSorting} /> ); @@ -105,7 +115,7 @@ export const AllPagesPanel: React.FC = (props) => { return; } editMenuProps?.onResetSettingsToDefault?.(); - const originItems = defaultMenuItems?.filter((item) => item.id !== ALL_PAGES_ID); + const originItems = defaultMenuItems?.filter(({id}) => id !== ALL_PAGES_ID); if (originItems) { onMenuItemsChanged(originItems); @@ -114,12 +124,12 @@ export const AllPagesPanel: React.FC = (props) => { const changeItemsOrder = useCallback( ({oldIndex, newIndex}: {oldIndex: number; newIndex: number}) => { - const newItems = menuItemsRef.current.filter((item) => item.id !== ALL_PAGES_ID); + const newItems = menuItemsRef.current.filter(({id}) => id !== ALL_PAGES_ID); const element = newItems.splice(oldIndex, 1)[0]; newItems.splice(newIndex, 0, element); - onMenuItemsChanged?.(newItems.filter((item) => item.type !== 'divider')); + onMenuItemsChanged?.(newItems.filter(({type}) => type !== 'divider')); setDraggingItemTitle(null); editMenuProps?.onChangeItemsOrder?.(element, oldIndex, newIndex); @@ -128,8 +138,9 @@ export const AllPagesPanel: React.FC = (props) => { ); const sortableItems = useMemo(() => { - return menuItemsRef.current.filter( - (item) => item.id !== ALL_PAGES_ID && !item.afterMoreButton && item.type !== 'divider', + return menuItems.filter( + ({id, afterMoreButton, type}) => + id !== ALL_PAGES_ID && !afterMoreButton && type !== 'divider', ); }, [menuItems]); diff --git a/src/components/AllPagesPanel/README.md b/src/components/AsideHeader/components/AllPagesPanel/README.md similarity index 63% rename from src/components/AllPagesPanel/README.md rename to src/components/AsideHeader/components/AllPagesPanel/README.md index 9d3a3456..8128ab3f 100644 --- a/src/components/AllPagesPanel/README.md +++ b/src/components/AsideHeader/components/AllPagesPanel/README.md @@ -15,9 +15,9 @@ import React from 'react'; import {AsideHeader, type AsideHeaderProps} from '@gravity-ui/navigation'; const DEFAULT_MENU_ITEMS: AsideHeaderProps['menuItems'] = [ - {id: 'home', title: 'Home', icon: 'home'}, - {id: 'analytics', title: 'Analytics', icon: 'chart'}, - {id: 'settings', title: 'Settings', icon: 'gear'}, + {item: {id: 'home', title: 'Home', icon: 'home'}}, + {item: {id: 'analytics', title: 'Analytics', icon: 'chart'}}, + {item: {id: 'settings', title: 'Settings', icon: 'gear'}}, ]; const Navigation: React.FC = ({children}) => { @@ -49,7 +49,7 @@ const useMenuItems = () => { const [menuItems, setMenuItems] = React.useState(DEFAULT_MENU_ITEMS); - const currentMenuItems = menuItems.map((item, index) => { + const currentMenuItems = menuItems.map((item, index) => { if ('type' in item || index > 5) { return item; } @@ -68,21 +68,21 @@ const useMenuItems = () => { ## Properties of `AsideHeader` -| Name | Description | Type | Default | -| :----------------- | :----------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------: | :-----: | -| defaultMenuItems | Default items in the navigation middle section | [`Array`](https://github.com/gravity-ui/navigation/blob/main/src/components/AsideHeader/README.md#menuitem) | `[]` | -| menuItems | Modifying items in the navigation middle section | [`Array`](https://github.com/gravity-ui/navigation/blob/main/src/components/AsideHeader/README.md#menuitem) | `[]` | -| editMenuProps | desc | `type` | | -| onMenuItemsChanged | Callback will be called when updating list of the menuItems in `AllPagesPanel` | `(items: Array) => void` | | +| Name | Description | Type | Default | +| :----------------- | :----------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------: | :-----: | +| defaultMenuItems | Default items in the navigation middle section | [`Array`](https://github.com/gravity-ui/navigation/blob/main/src/components/AsideHeader/README.md#menuitem) | `[]` | +| menuItems | Modifying items in the navigation middle section | [`Array`](https://github.com/gravity-ui/navigation/blob/main/src/components/AsideHeader/README.md#menuitem) | `[]` | +| editMenuProps | desc | `type` | | +| onMenuItemsChanged | Callback will be called when updating list of the menuItems in `AllPagesPanel` | `(items: Array) => void` | | ### `EditMenuProps` Provides settings and callbacks for managing panel and menu items in the `AsideHeader`. Callbacks are optional, you can managing with `AsideHeader.onMenuItemsChanged` prop. -| Name | Description | Type | Default | -| :----------------------- | :------------------------------------------------------- | :--------------------------------------------------------------------: | :-----: | -| enableSorting | Enable sorting functionality in the panel | `boolean` | | -| onOpenEditMode | Callback triggered when the edit mode is enabled | `() => void` | | -| onToggleMenuItem | Callback triggered when the menu item visible is toggled | `(changedItem: MenuItem) => void` | | -| onResetSettingsToDefault | Callback triggered when settings are reset to default | `() => void` | | -| onChangeItemsOrder | Callback triggered when the order of items is changed | `(changedItem: MenuItem, oldIndex: number, newIndex: number) => void;` | | +| Name | Description | Type | Default | +| :----------------------- | :------------------------------------------------------- | :---------------------------------------------------------------------------: | :-----: | +| enableSorting | Enable sorting functionality in the panel | `boolean` | | +| onOpenEditMode | Callback triggered when the edit mode is enabled | `() => void` | | +| onToggleMenuItem | Callback triggered when the menu item visible is toggled | `(changedItem: AsideHeaderItem) => void` | | +| onResetSettingsToDefault | Callback triggered when settings are reset to default | `() => void` | | +| onChangeItemsOrder | Callback triggered when the order of items is changed | `(changedItem: AsideHeaderItem, oldIndex: number, newIndex: number) => void;` | | diff --git a/src/components/AllPagesPanel/constants.ts b/src/components/AsideHeader/components/AllPagesPanel/constants.ts similarity index 74% rename from src/components/AllPagesPanel/constants.ts rename to src/components/AsideHeader/components/AllPagesPanel/constants.ts index 9c536946..be0488e8 100644 --- a/src/components/AllPagesPanel/constants.ts +++ b/src/components/AsideHeader/components/AllPagesPanel/constants.ts @@ -1,12 +1,12 @@ import {Ellipsis} from '@gravity-ui/icons'; -import {MenuItem} from '../types'; +import {AsideHeaderItem} from '../../types'; import i18n from './i18n'; export const ALL_PAGES_ID = 'all-pages' as const; -export function getAllPagesMenuItem(): MenuItem { +export function getAllPagesMenuItem(): AsideHeaderItem { return { id: ALL_PAGES_ID, title: i18n('menu-item.all-pages.title'), diff --git a/src/components/AllPagesPanel/i18n/en.json b/src/components/AsideHeader/components/AllPagesPanel/i18n/en.json similarity index 100% rename from src/components/AllPagesPanel/i18n/en.json rename to src/components/AsideHeader/components/AllPagesPanel/i18n/en.json diff --git a/src/components/AllPagesPanel/i18n/index.ts b/src/components/AsideHeader/components/AllPagesPanel/i18n/index.ts similarity index 82% rename from src/components/AllPagesPanel/i18n/index.ts rename to src/components/AsideHeader/components/AllPagesPanel/i18n/index.ts index 0cd1e48d..c38797ac 100644 --- a/src/components/AllPagesPanel/i18n/index.ts +++ b/src/components/AsideHeader/components/AllPagesPanel/i18n/index.ts @@ -1,6 +1,6 @@ import {addComponentKeysets} from '@gravity-ui/uikit/i18n'; -import {NAMESPACE} from '../../utils/cn'; +import {NAMESPACE} from '../../../../utils/cn'; import en from './en.json'; import ru from './ru.json'; diff --git a/src/components/AllPagesPanel/i18n/ru.json b/src/components/AsideHeader/components/AllPagesPanel/i18n/ru.json similarity index 100% rename from src/components/AllPagesPanel/i18n/ru.json rename to src/components/AsideHeader/components/AllPagesPanel/i18n/ru.json diff --git a/src/components/AllPagesPanel/index.ts b/src/components/AsideHeader/components/AllPagesPanel/index.ts similarity index 100% rename from src/components/AllPagesPanel/index.ts rename to src/components/AsideHeader/components/AllPagesPanel/index.ts diff --git a/src/components/AsideHeader/components/AllPagesPanel/useGroupedMenuItems.ts b/src/components/AsideHeader/components/AllPagesPanel/useGroupedMenuItems.ts new file mode 100644 index 00000000..ac27961c --- /dev/null +++ b/src/components/AsideHeader/components/AllPagesPanel/useGroupedMenuItems.ts @@ -0,0 +1,38 @@ +import {useMemo} from 'react'; + +import {AsideHeaderItem} from '../../types'; + +import {ALL_PAGES_ID} from './constants'; +import i18n from './i18n'; + +export const useGroupedMenuItems = (asideHeaderItems: AsideHeaderItem[]) => { + const allPagesMenuItems = useMemo(() => { + const filteredItems = asideHeaderItems.filter( + ({id, type}) => type !== 'divider' && id !== ALL_PAGES_ID, + ); + filteredItems.sort(({type: typeA}, {type: typeB}) => { + if (typeA === 'action') { + return 1; + } + if (typeB === 'action') { + return -1; + } + return 0; + }); + const groupedItems = filteredItems.reduce( + (acc, asideHeaderItem) => { + const category = + asideHeaderItem.category || i18n('all-panel.menu.category.allOther'); + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(asideHeaderItem); + return acc; + }, + {} as {[key: string]: AsideHeaderItem[]}, + ); + return groupedItems; + }, [asideHeaderItems]); + + return allPagesMenuItems; +}; diff --git a/src/components/AsideHeader/components/AllPagesPanel/useVisibleMenuItems.ts b/src/components/AsideHeader/components/AllPagesPanel/useVisibleMenuItems.ts new file mode 100644 index 00000000..a232eca2 --- /dev/null +++ b/src/components/AsideHeader/components/AllPagesPanel/useVisibleMenuItems.ts @@ -0,0 +1,31 @@ +import {useMemo} from 'react'; + +import {useAsideHeaderInnerContext} from '../../AsideHeaderContext'; +import {AsideHeaderItem} from '../../types'; + +export const useVisibleMenuItems = (): AsideHeaderItem[] => { + const {menuItems, allPagesIsAvailable} = useAsideHeaderInnerContext(); + return useMemo(() => { + if (!allPagesIsAvailable) { + return menuItems; + } + let lastVisibleIndex = 0; + return menuItems.filter( + ({type, hidden}: AsideHeaderItem, index: number, items: AsideHeaderItem[]): boolean => { + if (hidden) { + return false; + } + + if ( + index > 0 && + type === 'divider' && + (items[lastVisibleIndex].type === 'divider' || items[lastVisibleIndex].hidden) + ) { + return false; + } + lastVisibleIndex = index; + return true; + }, + ); + }, [allPagesIsAvailable, menuItems]); +}; diff --git a/src/components/CompositeBar/CompositeBar.scss b/src/components/AsideHeader/components/CompositeBar/CompositeBar.scss similarity index 88% rename from src/components/CompositeBar/CompositeBar.scss rename to src/components/AsideHeader/components/CompositeBar/CompositeBar.scss index 849c2aba..796db16a 100644 --- a/src/components/CompositeBar/CompositeBar.scss +++ b/src/components/AsideHeader/components/CompositeBar/CompositeBar.scss @@ -1,4 +1,4 @@ -@use '../variables'; +@use '../../../variables'; $block: '.#{variables.$ns}composite-bar'; diff --git a/src/components/CompositeBar/CompositeBar.tsx b/src/components/AsideHeader/components/CompositeBar/CompositeBar.tsx similarity index 82% rename from src/components/CompositeBar/CompositeBar.tsx rename to src/components/AsideHeader/components/CompositeBar/CompositeBar.tsx index 60a93884..9ae5f1da 100644 --- a/src/components/CompositeBar/CompositeBar.tsx +++ b/src/components/AsideHeader/components/CompositeBar/CompositeBar.tsx @@ -3,10 +3,9 @@ import React, {FC, ReactNode, useCallback, useContext, useRef} from 'react'; import {List} from '@gravity-ui/uikit'; import AutoSizer, {Size} from 'react-virtualized-auto-sizer'; -import {useAsideHeaderContext} from '../AsideHeader/AsideHeaderContext'; -import {ASIDE_HEADER_COMPACT_WIDTH} from '../constants'; -import {MenuItem, SubheaderMenuItem} from '../types'; -import {block} from '../utils/cn'; +import {ASIDE_HEADER_COMPACT_WIDTH} from '../../../constants'; +import {block} from '../../../utils/cn'; +import {AsideHeaderItem} from '../../types'; import {Item, ItemProps} from './Item/Item'; import {MultipleTooltip, MultipleTooltipContext, MultipleTooltipProvider} from './MultipleTooltip'; @@ -18,33 +17,29 @@ import { getItemsMinHeight, getMoreButtonItem, getSelectedItemIndex, - isMenuItem, } from './utils'; import './CompositeBar.scss'; const b = block('composite-bar'); -export type CompositeBarItem = MenuItem | SubheaderMenuItem; - -type CompositeBarItems = - | {type: 'menu'; items: MenuItem[]} - | {type: 'subheader'; items: SubheaderMenuItem[]}; - -export type CompositeBarProps = CompositeBarItems & { +export type CompositeBarProps = { + type: 'menu' | 'subheader'; + items: AsideHeaderItem[]; onItemClick?: ( - item: MenuItem, + item: AsideHeaderItem, collapsed: boolean, event: React.MouseEvent, ) => void; multipleTooltip?: boolean; menuMoreTitle?: string; onMoreClick?: () => void; + compact: boolean; compositeId?: string; }; type CompositeBarViewProps = CompositeBarProps & { - collapseItems?: MenuItem[]; + collapseItems?: AsideHeaderItem[]; compositeId?: string; }; @@ -55,9 +50,10 @@ const CompositeBarView: FC = ({ onMoreClick, collapseItems, multipleTooltip = false, + compact, compositeId, }) => { - const ref = useRef>(null); + const ref = useRef>(null); const tooltipRef = useRef(null); const { @@ -66,7 +62,6 @@ const CompositeBarView: FC = ({ activeIndex, lastClickedItemIndex, } = useContext(MultipleTooltipContext); - const {compact} = useAsideHeaderContext(); React.useEffect(() => { function handleBlurWindow() { @@ -166,7 +161,10 @@ const CompositeBarView: FC = ({ ]); const onItemClickByIndex = useCallback( - (itemIndex: number): ItemProps['onItemClick'] => + ( + itemIndex: number, + orginalItemClick: AsideHeaderItem['onItemClick'], + ): ItemProps['onItemClick'] => (item, collapsed, event) => { if ( compact && @@ -179,7 +177,7 @@ const CompositeBarView: FC = ({ active: false, }); } - onItemClick?.(item, collapsed, event); + onItemClick?.({...item, onItemClick: orginalItemClick}, collapsed, event); }, [ compact, @@ -197,7 +195,7 @@ const CompositeBarView: FC = ({ onMouseEnter={onTooltipMouseEnter} onMouseLeave={onTooltipMouseLeave} > - + id={compositeId} ref={ref} items={items} @@ -208,24 +206,17 @@ const CompositeBarView: FC = ({ virtualized={false} filterable={false} sortable={false} - renderItem={(item, _isItemActive, itemIndex) => { - const itemExtraProps = isMenuItem(item) ? {item} : item; - const enableTooltip = isMenuItem(item) - ? !multipleTooltip - : item.enableTooltip; - - return ( - - ); - }} + renderItem={(item, _isItemActive, itemIndex) => ( + + )} /> {type === 'menu' && multipleTooltip && ( @@ -247,6 +238,7 @@ export const CompositeBar: FC = ({ onItemClick, onMoreClick, multipleTooltip = false, + compact, compositeId, }) => { if (items.length === 0) { @@ -275,6 +267,7 @@ export const CompositeBar: FC = ({ = ({ } else { node = (
- +
); } diff --git a/src/components/CompositeBar/HighlightedItem/HighlightedItem.scss b/src/components/AsideHeader/components/CompositeBar/HighlightedItem/HighlightedItem.scss similarity index 98% rename from src/components/CompositeBar/HighlightedItem/HighlightedItem.scss rename to src/components/AsideHeader/components/CompositeBar/HighlightedItem/HighlightedItem.scss index fa20b0b3..132dff4f 100644 --- a/src/components/CompositeBar/HighlightedItem/HighlightedItem.scss +++ b/src/components/AsideHeader/components/CompositeBar/HighlightedItem/HighlightedItem.scss @@ -1,4 +1,4 @@ -@use '../../variables'; +@use '../../../../variables'; $block: '.#{variables.$ns}composite-bar-highlighted-item'; diff --git a/src/components/CompositeBar/HighlightedItem/HighlightedItem.tsx b/src/components/AsideHeader/components/CompositeBar/HighlightedItem/HighlightedItem.tsx similarity index 87% rename from src/components/CompositeBar/HighlightedItem/HighlightedItem.tsx rename to src/components/AsideHeader/components/CompositeBar/HighlightedItem/HighlightedItem.tsx index fefb77e5..7d9d8fbe 100644 --- a/src/components/CompositeBar/HighlightedItem/HighlightedItem.tsx +++ b/src/components/AsideHeader/components/CompositeBar/HighlightedItem/HighlightedItem.tsx @@ -3,8 +3,8 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Portal} from '@gravity-ui/uikit'; import debounceFn from 'lodash/debounce'; -import {useAsideHeaderInnerContext} from '../../AsideHeader/AsideHeaderContext'; -import {block} from '../../utils/cn'; +import {block} from '../../../../utils/cn'; +import {useAsideHeaderInnerContext} from '../../../AsideHeaderContext'; import './HighlightedItem.scss'; @@ -26,7 +26,7 @@ export const HighlightedItem: React.FC = ({ onClickCapture, }: ItemInnerProps) => { const {openModalSubscriber} = useAsideHeaderInnerContext(); - const [{top, left, width, height}, setPosition] = useState({ + const [position, setPosition] = useState({ top: 0, left: 0, width: 0, @@ -62,13 +62,16 @@ export const HighlightedItem: React.FC = ({ useEffect(() => { if (!isModalOpen) { - return; + return undefined; } handleResize(); window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; }, [handleResize, isModalOpen]); openModalSubscriber?.((open: boolean) => { @@ -83,7 +86,7 @@ export const HighlightedItem: React.FC = ({
React.ReactNode; - /** - * This callback will be called when Escape key pressed on keyboard, or click outside was made - * This behaviour could be disabled with `disableEscapeKeyDown` - * and `disableOutsideClick` options - * - * @deprecated Use `onOpenChangePopup` instead - */ - onClosePopup?: () => void; - onOpenChangePopup?: PopupProps['onOpenChange']; -} - -export interface ItemProps extends ItemPopup { - item: MenuItem; - enableTooltip?: boolean; - onItemClick?: ( - item: MenuItem, - collapsed: boolean, - event: React.MouseEvent, - ) => void; - onItemClickCapture?: (event: React.SyntheticEvent) => void; - onCollapseItemClick?: () => void; - bringForward?: boolean; -} +export interface ItemProps extends AsideHeaderItem {} interface ItemInnerProps extends ItemProps { className?: string; - collapseItems?: MenuItem[]; + collapseItems?: AsideHeaderItem[]; onMouseEnter?: () => void; onMouseLeave?: () => void; } -function renderItemTitle(item: MenuItem) { - let titleNode =
{item.title}
; +function renderItemTitle(params: Pick) { + let titleNode =
{params.title}
; - if (item.rightAdornment) { + if (params.rightAdornment) { titleNode = ( {titleNode} -
{item.rightAdornment}
+
{params.rightAdornment}
); } @@ -83,59 +50,46 @@ export const defaultPopupOffset: NonNullable = {mainAxis: export const Item: React.FC = (props) => { const { - item, className, collapseItems, + compact, onMouseLeave, onMouseEnter, enableTooltip = true, popupVisible = false, - popupAnchor, - popupAnchorElement, + popupRef: anchoreRefProp, popupPlacement = defaultPopupPlacement, popupOffset = defaultPopupOffset, popupKeepMounted, renderPopupContent, - onClosePopup, onOpenChangePopup, onItemClick, onItemClickCapture, onCollapseItemClick, + itemWrapper, bringForward, + rightAdornment, + title, + link, + qa, } = props; - const {compact} = useAsideHeaderContext(); - const [open, toggleOpen] = React.useState(false); const ref = React.useRef(null); - const anchorRef = popupAnchorElement ? {current: popupAnchorElement} : popupAnchor || ref; + const anchorRef = anchoreRefProp?.current ? anchoreRefProp : ref; const highlightedRef = React.useRef(null); - const type = item.type || ITEM_TYPE_REGULAR; - const current = item.current || false; - const tooltipText = item.tooltipText || item.title; - const icon = item.icon; - const iconSize = item.iconSize || ASIDE_HEADER_ICON_SIZE; - const iconQa = item.iconQa; - const collapsedItem = item.id === COLLAPSE_ITEM_ID; - - const handleClosePopup = React.useCallback( - (event: MouseEvent | KeyboardEvent) => { - if ( - event instanceof MouseEvent && - event.target && - ref.current?.contains(event.target as Node) - ) { - return; - } - onClosePopup?.(); - }, - [onClosePopup], - ); + const type = props.type || ITEM_TYPE_REGULAR; + const current = props.current || false; + const tooltipText = props.tooltipText || props.title; + 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>( - (open, event, reason) => { + (newOpen, event, reason) => { if ( event instanceof MouseEvent && event.target && @@ -143,12 +97,12 @@ export const Item: React.FC = (props) => { ) { return; } - onOpenChangePopup?.(open, event, reason); + onOpenChangePopup?.(newOpen, event, reason); }, - [onClosePopup], + [onOpenChangePopup], ); - if (item.type === 'divider') { + if (type === 'divider') { return
; } @@ -175,9 +129,7 @@ export const Item: React.FC = (props) => { }; const makeNode = ({icon: iconEl, title: titleEl}: MakeItemParams) => { - const [Tag, tagProps] = item.link - ? ['a' as const, {href: item.link}] - : ['button' as const, {}]; + const [Tag, tagProps] = link ? ['a' as const, {href: link}] : ['button' as const, {}]; const createdNode = ( @@ -185,7 +137,7 @@ export const Item: React.FC = (props) => { {...tagProps} className={b({type, current, compact}, className)} ref={ref} - data-qa={item.qa} + data-qa={qa} onClick={(event: React.MouseEvent) => { if (collapsedItem) { /** @@ -196,7 +148,7 @@ export const Item: React.FC = (props) => { toggleOpen(!open); onCollapseItemClick?.(); } else { - onItemClick?.(item, false, event); + onItemClick?.(props, false, event); } }} onClickCapture={onItemClickCapture} @@ -217,7 +169,7 @@ export const Item: React.FC = (props) => {
{titleEl}
@@ -229,8 +181,7 @@ export const Item: React.FC = (props) => { keepMounted={popupKeepMounted} placement={popupPlacement} offset={popupOffset} - anchorRef={anchorRef} - onClose={handleClosePopup} + anchorElement={anchorRef.current} onOpenChange={handleOpenChangePopup} > {renderPopupContent()} @@ -245,18 +196,18 @@ export const Item: React.FC = (props) => { const iconNode = icon ? ( ) : null; - const titleNode = renderItemTitle(item); + const titleNode = renderItemTitle({title, rightAdornment}); const params = {icon: iconNode, title: titleNode}; let highlightedNode = null; let node; - const opts = {compact: Boolean(compact), collapsed: false, item, ref}; + const opts = {compact: Boolean(compact), collapsed: false, item: props, ref}; - if (typeof item.itemWrapper === 'function') { - node = item.itemWrapper(params, makeNode, opts) as React.ReactElement; + if (typeof itemWrapper === 'function') { + node = itemWrapper(params, makeNode, opts) as React.ReactElement; highlightedNode = bringForward && - (item.itemWrapper( + (itemWrapper( params, ({icon: iconEl}) => makeIconNode(iconEl), opts, @@ -273,14 +224,18 @@ export const Item: React.FC = (props) => { iconNode={highlightedNode} iconRef={highlightedRef} onClick={(event: React.MouseEvent) => - onItemClick?.(item, false, event) + onItemClick?.(props, false, event) } onClickCapture={onItemClickCapture} /> )} {node} {open && collapsedItem && collapseItems?.length && Boolean(anchorRef?.current) && ( - toggleOpen(false)} /> + toggleOpen(false)} + /> )}
); @@ -290,14 +245,14 @@ Item.displayName = 'Item'; interface CollapsedPopupProps { anchorRef: React.RefObject; - onClose: () => void; + onOpenChange: () => void; } function CollapsedPopup({ onItemClick, collapseItems, anchorRef, - onClose, + onOpenChange, }: ItemInnerProps & CollapsedPopupProps) { const {compact} = useAsideHeaderContext(); return collapseItems?.length ? ( @@ -305,8 +260,8 @@ function CollapsedPopup({ strategy="fixed" placement={POPUP_PLACEMENT} open={true} - anchorRef={anchorRef} - onClose={onClose} + anchorElement={anchorRef.current} + onOpenChange={onOpenChange} >
{ + onItemClick={onOpenChange} + renderItem={(item) => { const makeCollapseNode = ({ title: titleEl, icon: iconEl, }: MakeItemParams) => { - const [Tag, tagProps] = collapseItem.link - ? ['a' as const, {href: collapseItem.link}] + const [Tag, tagProps] = item.link + ? ['a' as const, {href: item.link}] : ['button' as const, {}]; return ( @@ -333,7 +288,7 @@ function CollapsedPopup({ {...tagProps} className={b('collapse-item')} onClick={(event: React.MouseEvent) => { - onItemClick?.(collapseItem, true, event); + onItemClick?.(item, true, event); }} > {iconEl} @@ -342,24 +297,20 @@ function CollapsedPopup({ ); }; - const titleNode = renderItemTitle(collapseItem); - const iconNode = collapseItem.icon && ( - + const titleNode = renderItemTitle(item); + const iconNode = item.icon && ( + ); const params = {title: titleNode, icon: iconNode}; const opts = { compact: Boolean(compact), collapsed: true, - item: collapseItem, + item, ref: anchorRef, }; - if (typeof collapseItem.itemWrapper === 'function') { - return collapseItem.itemWrapper(params, makeCollapseNode, opts); + if (typeof item.itemWrapper === 'function') { + return item.itemWrapper(params, makeCollapseNode, opts); } else { return makeCollapseNode(params); } diff --git a/src/components/CompositeBar/MultipleTooltip/MultipleTooltip.scss b/src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltip.scss similarity index 98% rename from src/components/CompositeBar/MultipleTooltip/MultipleTooltip.scss rename to src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltip.scss index 6e261357..60f2832e 100644 --- a/src/components/CompositeBar/MultipleTooltip/MultipleTooltip.scss +++ b/src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltip.scss @@ -1,4 +1,4 @@ -@use '../../variables'; +@use '../../../../variables'; $block: '.#{variables.$ns}multiple-tooltip'; diff --git a/src/components/CompositeBar/MultipleTooltip/MultipleTooltip.tsx b/src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltip.tsx similarity index 76% rename from src/components/CompositeBar/MultipleTooltip/MultipleTooltip.tsx rename to src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltip.tsx index 963cee51..648de5f5 100644 --- a/src/components/CompositeBar/MultipleTooltip/MultipleTooltip.tsx +++ b/src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltip.tsx @@ -2,8 +2,9 @@ import React from 'react'; import {Popup, PopupProps} from '@gravity-ui/uikit'; -import {MenuItem} from '../../types'; -import {block} from '../../utils/cn'; +import {AsideHeaderItem} from 'src/components/AsideHeader/types'; + +import {block} from '../../../../utils/cn'; import {COLLAPSE_ITEM_ID} from '../constants'; import {MultipleTooltipContext} from './MultipleTooltipContext'; @@ -14,8 +15,9 @@ const b = block('multiple-tooltip'); const POPUP_OFFSET: PopupProps['offset'] = {mainAxis: 4, crossAxis: -32}; -export type MultipleTooltipProps = Pick & { - items: MenuItem[]; +export type MultipleTooltipProps = Pick & { + anchorRef: React.RefObject; + items: AsideHeaderItem[]; }; export const MultipleTooltip: React.FC = ({ @@ -31,7 +33,7 @@ export const MultipleTooltip: React.FC = ({ = ({ !hideCollapseItemTooltip || (id !== COLLAPSE_ITEM_ID && type !== 'action'), ) - .map((item, idx) => { - switch (item.type) { + .map((currentItem, idx) => { + switch (currentItem.type) { case 'divider': return (
- {item.title} + {currentItem.title}
); default: return (
- {item.title} + {currentItem.title}
); } diff --git a/src/components/CompositeBar/MultipleTooltip/MultipleTooltipContext.tsx b/src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltipContext.tsx similarity index 100% rename from src/components/CompositeBar/MultipleTooltip/MultipleTooltipContext.tsx rename to src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltipContext.tsx index 817ebead..b802220b 100644 --- a/src/components/CompositeBar/MultipleTooltip/MultipleTooltipContext.tsx +++ b/src/components/AsideHeader/components/CompositeBar/MultipleTooltip/MultipleTooltipContext.tsx @@ -34,10 +34,6 @@ export class MultipleTooltipProvider extends React.PureComponent< ...multipleTooltipContextDefaults, }; - setValue: MultipleTooltipContextProps['setValue'] = (value) => { - this.setState({...value}); - }; - render() { const {children} = this.props; @@ -47,4 +43,8 @@ export class MultipleTooltipProvider extends React.PureComponent< ); } + + setValue: MultipleTooltipContextProps['setValue'] = (value) => { + this.setState({...value}); + }; } diff --git a/src/components/CompositeBar/MultipleTooltip/index.ts b/src/components/AsideHeader/components/CompositeBar/MultipleTooltip/index.ts similarity index 100% rename from src/components/CompositeBar/MultipleTooltip/index.ts rename to src/components/AsideHeader/components/CompositeBar/MultipleTooltip/index.ts diff --git a/src/components/CompositeBar/constants.ts b/src/components/AsideHeader/components/CompositeBar/constants.ts similarity index 100% rename from src/components/CompositeBar/constants.ts rename to src/components/AsideHeader/components/CompositeBar/constants.ts diff --git a/src/components/AsideHeader/components/CompositeBar/index.ts b/src/components/AsideHeader/components/CompositeBar/index.ts new file mode 100644 index 00000000..36a544fe --- /dev/null +++ b/src/components/AsideHeader/components/CompositeBar/index.ts @@ -0,0 +1,2 @@ +export {CompositeBar} from './CompositeBar'; +export type {CompositeBarProps} from './CompositeBar'; diff --git a/src/components/AsideHeader/components/CompositeBar/utils.ts b/src/components/AsideHeader/components/CompositeBar/utils.ts new file mode 100644 index 00000000..24e9040e --- /dev/null +++ b/src/components/AsideHeader/components/CompositeBar/utils.ts @@ -0,0 +1,119 @@ +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': + return 50; + case 'divider': + return 15; + + default: + return ITEM_HEIGHT; + } +} + +export function getItemsHeight(items: T[]) { + return items.reduce((sum, item) => sum + getItemHeight(item), 0); +} + +export function getSelectedItemIndex(compositeItems: AsideHeaderItem[]) { + const index = compositeItems.findIndex(({current}) => Boolean(current)); + 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[]; +} { + 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}; +} diff --git a/src/components/AsideHeader/components/FirstPanel.tsx b/src/components/AsideHeader/components/FirstPanel.tsx index fad81b06..e6d1563c 100644 --- a/src/components/AsideHeader/components/FirstPanel.tsx +++ b/src/components/AsideHeader/components/FirstPanel.tsx @@ -2,13 +2,13 @@ import React, {useRef} from 'react'; import {setRef} from '@gravity-ui/uikit'; -import {useVisibleMenuItems} from '../../AllPagesPanel'; -import {CompositeBar} from '../../CompositeBar/CompositeBar'; import {useAsideHeaderInnerContext} from '../AsideHeaderContext'; import i18n from '../i18n'; import {b} from '../utils'; +import {useVisibleMenuItems} from './AllPagesPanel'; import {CollapseButton} from './CollapseButton/CollapseButton'; +import {CompositeBar} from './CompositeBar'; import {Header} from './Header'; import {Panels} from './Panels'; @@ -54,6 +54,7 @@ export const FirstPanel = React.forwardRef((_props, ref) => { + ); +} diff --git a/src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-chromium-linux.png b/src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-chromium-linux.png similarity index 100% rename from src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-chromium-linux.png rename to src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-chromium-linux.png diff --git a/src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-webkit-linux.png b/src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-webkit-linux.png similarity index 100% rename from src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-webkit-linux.png rename to src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-dark-webkit-linux.png diff --git a/src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-chromium-linux.png b/src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-chromium-linux.png similarity index 100% rename from src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-chromium-linux.png rename to src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-chromium-linux.png diff --git a/src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-webkit-linux.png b/src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-webkit-linux.png similarity index 100% rename from src/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-webkit-linux.png rename to src/components/AsideHeader/components/FooterItem/__snapshots__/FooterItem.visual.test.tsx-snapshots/FooterItem-render-story-Default-light-webkit-linux.png diff --git a/src/components/FooterItem/__stories__/FooterItem.stories.tsx b/src/components/AsideHeader/components/FooterItem/__stories__/FooterItem.stories.tsx similarity index 81% rename from src/components/FooterItem/__stories__/FooterItem.stories.tsx rename to src/components/AsideHeader/components/FooterItem/__stories__/FooterItem.stories.tsx index f6502343..2e0f8159 100644 --- a/src/components/FooterItem/__stories__/FooterItem.stories.tsx +++ b/src/components/AsideHeader/components/FooterItem/__stories__/FooterItem.stories.tsx @@ -3,9 +3,9 @@ import React from 'react'; import {Gear} from '@gravity-ui/icons'; import type {Meta, StoryFn} from '@storybook/react'; -import {AsideHeaderContextProvider} from '../../AsideHeader/AsideHeaderContext'; -import {EMPTY_CONTEXT_VALUE} from '../../AsideHeader/__stories__/moc'; -import {ASIDE_HEADER_COMPACT_WIDTH, ASIDE_HEADER_EXPANDED_WIDTH} from '../../constants'; +import {ASIDE_HEADER_COMPACT_WIDTH, ASIDE_HEADER_EXPANDED_WIDTH} from '../../../../constants'; +import {AsideHeaderContextProvider} from '../../../AsideHeaderContext'; +import {EMPTY_CONTEXT_VALUE} from '../../../__stories__/moc'; import {FooterItem, FooterItemProps} from '../FooterItem'; import './FooterItemShowcase.scss'; @@ -38,9 +38,7 @@ const Template: StoryFn = (args) => ; export const Default = Template.bind({}); Default.args = { compact: false, - item: { - id: 'settings', - title: 'Settings', - icon: Gear, - }, + id: 'settings', + title: 'Settings', + icon: Gear, }; diff --git a/src/components/FooterItem/__stories__/FooterItemShowcase.scss b/src/components/AsideHeader/components/FooterItem/__stories__/FooterItemShowcase.scss similarity index 59% rename from src/components/FooterItem/__stories__/FooterItemShowcase.scss rename to src/components/AsideHeader/components/FooterItem/__stories__/FooterItemShowcase.scss index f8a82b55..f01cde1c 100644 --- a/src/components/FooterItem/__stories__/FooterItemShowcase.scss +++ b/src/components/AsideHeader/components/FooterItem/__stories__/FooterItemShowcase.scss @@ -1,10 +1,10 @@ -@import '../../../../styles/mixins'; +@use '../../../../../../styles/mixins'; .footer-item-showcase { --gn-aside-header-min-width: 56px; &__content { - @include text-body-3; + @include mixins.text-body-3; padding: 40px; } } diff --git a/src/components/FooterItem/__tests__/FooterItem.visual.test.tsx b/src/components/AsideHeader/components/FooterItem/__tests__/FooterItem.visual.test.tsx similarity index 100% rename from src/components/FooterItem/__tests__/FooterItem.visual.test.tsx rename to src/components/AsideHeader/components/FooterItem/__tests__/FooterItem.visual.test.tsx diff --git a/src/components/FooterItem/__tests__/helpersPlaywright.ts b/src/components/AsideHeader/components/FooterItem/__tests__/helpersPlaywright.ts similarity index 100% rename from src/components/FooterItem/__tests__/helpersPlaywright.ts rename to src/components/AsideHeader/components/FooterItem/__tests__/helpersPlaywright.ts diff --git a/src/components/AsideHeader/components/Header.tsx b/src/components/AsideHeader/components/Header.tsx index 3e7d5d03..a6f26406 100644 --- a/src/components/AsideHeader/components/Header.tsx +++ b/src/components/AsideHeader/components/Header.tsx @@ -2,16 +2,17 @@ import React, {useCallback} from 'react'; import {Icon} from '@gravity-ui/uikit'; -import {CompositeBar} from '../../CompositeBar/CompositeBar'; import {Logo} from '../../Logo'; import {ASIDE_HEADER_COMPACT_WIDTH, HEADER_DIVIDER_HEIGHT} from '../../constants'; -import {SubheaderMenuItem} from '../../types'; import {useAsideHeaderContext, useAsideHeaderInnerContext} from '../AsideHeaderContext'; +import {AsideHeaderItem} from '../types'; import {b} from '../utils'; +import {CompositeBar} from './CompositeBar'; + import headerDividerCollapsedIcon from '../../../../assets/icons/divider-collapsed.svg'; -const DEFAULT_SUBHEADER_ITEMS: SubheaderMenuItem[] = []; +const DEFAULT_SUBHEADER_ITEMS: AsideHeaderItem[] = []; const HEADER_COMPOSITE_ID = 'gravity-ui/navigation-header-composite-bar'; export const Header = () => { @@ -42,6 +43,7 @@ export const Header = () => { diff --git a/src/components/AsideHeader/index.ts b/src/components/AsideHeader/index.ts new file mode 100644 index 00000000..287884ab --- /dev/null +++ b/src/components/AsideHeader/index.ts @@ -0,0 +1,7 @@ +export {AsideHeader} from './AsideHeader'; +export type {AsideHeaderProps} from './types'; +export {AsideHeaderContextProvider, useAsideHeaderContext} from './AsideHeaderContext'; +export {FooterItem, type FooterItemProps} from './components/FooterItem/FooterItem'; +export {PageLayout, type PageLayoutProps} from './components/PageLayout/PageLayout'; +export {PageLayoutAside} from './components/PageLayout/PageLayoutAside'; +export {AsideFallback} from './components/PageLayout/AsideFallback'; diff --git a/src/components/AsideHeader/types.tsx b/src/components/AsideHeader/types.tsx index a7f1b050..806aff14 100644 --- a/src/components/AsideHeader/types.tsx +++ b/src/components/AsideHeader/types.tsx @@ -1,8 +1,8 @@ -import {QAProps} from '@gravity-ui/uikit'; +import {PopupProps, QAProps} from '@gravity-ui/uikit'; import {RenderContentType} from '../Content'; import {DrawerItemProps} from '../Drawer/Drawer'; -import {LogoProps, MenuItem, OpenModalSubscriber, SubheaderMenuItem, TopAlertProps} from '../types'; +import {LogoProps, MenuItem, OpenModalSubscriber, TopAlertProps} from '../types'; import {AsideHeaderContextType} from './AsideHeaderContext'; @@ -14,10 +14,10 @@ export interface LayoutProps { export interface EditMenuProps { onOpenEditMode?: () => void; - onToggleMenuItem?: (changedItem: MenuItem) => void; + onToggleMenuItem?: (changedItem: AsideHeaderItem) => void; onResetSettingsToDefault?: () => void; enableSorting?: boolean; - onChangeItemsOrder?: (changedItem: MenuItem, oldIndex: number, newIndex: number) => void; + onChangeItemsOrder?: (changedItem: AsideHeaderItem, oldIndex: number, newIndex: number) => void; } export interface AsideHeaderGeneralProps extends QAProps { @@ -54,10 +54,10 @@ export interface AsideHeaderGeneralProps extends QAProps { export interface AsideHeaderDefaultProps { panelItems?: DrawerItemProps[]; - subheaderItems?: SubheaderMenuItem[]; - menuItems?: MenuItem[]; - defaultMenuItems?: MenuItem[]; - onMenuItemsChanged?: (items: MenuItem[]) => void; + subheaderItems?: AsideHeaderItem[]; + menuItems?: AsideHeaderItem[]; + defaultMenuItems?: AsideHeaderItem[]; + onMenuItemsChanged?: (items: AsideHeaderItem[]) => void; headerDecoration?: boolean; } @@ -73,3 +73,33 @@ export interface AsideHeaderProps export enum InnerPanels { AllPages = 'all-pages', } + +interface ItemPopup { + popupVisible?: PopupProps['open']; + /** + * floating element anchor ref object + * */ + popupRef?: React.RefObject; + popupPlacement?: PopupProps['placement']; + popupOffset?: PopupProps['offset']; + popupKeepMounted?: PopupProps['keepMounted']; + renderPopupContent?: () => React.ReactNode; + /** + * This callback will be called when Escape key pressed on keyboard, or click outside was made + * This behaviour could be disabled with `disableEscapeKeyDown` + * and `disableOutsideClick` options + */ + onOpenChangePopup?: PopupProps['onOpenChange']; +} + +export interface AsideHeaderItem extends ItemPopup, MenuItem { + enableTooltip?: boolean; + onItemClick?: ( + item: AsideHeaderItem, + collapsed: boolean, + event: React.MouseEvent, + ) => void; + onCollapseItemClick?: () => void; + bringForward?: boolean; + compact?: boolean; +} diff --git a/src/components/AsideHeader/useAsideHeaderInnerContextValue.tsx b/src/components/AsideHeader/useAsideHeaderInnerContextValue.tsx index af88addc..632dd456 100644 --- a/src/components/AsideHeader/useAsideHeaderInnerContextValue.tsx +++ b/src/components/AsideHeader/useAsideHeaderInnerContextValue.tsx @@ -1,12 +1,12 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {AllPagesPanel, getAllPagesMenuItem} from '../AllPagesPanel'; import {MenuItem} from '../types'; import {AsideHeaderInnerContextType} from './AsideHeaderContext'; -import {AsideHeaderProps, InnerPanels} from './types'; +import {AllPagesPanel, getAllPagesMenuItem} from './components/AllPagesPanel'; +import {AsideHeaderItem, AsideHeaderProps, InnerPanels} from './types'; -const EMPTY_MENU_ITEMS: MenuItem[] = []; +const EMPTY_MENU_ITEMS: AsideHeaderItem[] = []; export const useAsideHeaderInnerContextValue = ( props: AsideHeaderProps & {size: number}, diff --git a/src/components/CompositeBar/utils.ts b/src/components/CompositeBar/utils.ts deleted file mode 100644 index bf50c6e2..00000000 --- a/src/components/CompositeBar/utils.ts +++ /dev/null @@ -1,124 +0,0 @@ -import {Ellipsis} from '@gravity-ui/icons'; - -import {CompositeBarItem} from '../CompositeBar/CompositeBar'; -import {ITEM_HEIGHT} from '../constants'; - -import {MenuItem} from './../types'; -import {COLLAPSE_ITEM_ID} from './constants'; - -export function getItemHeight(item: CompositeBarItem) { - if (!isMenuItem(item)) { - return ITEM_HEIGHT; - } - - switch (item.type) { - case 'action': - return 50; - case 'divider': - return 15; - - default: - return ITEM_HEIGHT; - } -} - -export function getItemsHeight(items: T[]) { - return items.reduce((sum, item) => sum + getItemHeight(item), 0); -} - -export function getSelectedItemIndex(items: MenuItem[]) { - const index = items.findIndex(({current}) => Boolean(current)); - return index === -1 ? undefined : index; -} - -export function getPinnedItems(items: MenuItem[]) { - const pinnedItems: MenuItem[] = []; - for (const item of items) { - if (item.pinned) { - pinnedItems.push(item); - } else if (item.type === 'divider') { - if (pinnedItems.length > 0 && pinnedItems[pinnedItems.length - 1].type !== 'divider') { - pinnedItems.push(item); - } - } - } - return pinnedItems; -} - -export function getItemsMinHeight(items: MenuItem[]) { - const pinnedItems = getPinnedItems(items); - const afterMoreButtonItems = items.filter((item) => item.afterMoreButton); - - return ( - getItemsHeight(pinnedItems) + - getItemsHeight(afterMoreButtonItems) + - (pinnedItems.length === items.length ? 0 : ITEM_HEIGHT) - ); -} - -export function getMoreButtonItem(menuMoreTitle?: string): MenuItem { - return { - id: COLLAPSE_ITEM_ID, - title: menuMoreTitle, - icon: Ellipsis, - iconSize: 18, - }; -} - -export function getAutosizeListItems( - items: MenuItem[], - height: number, - collapseItem: MenuItem, -): { - listItems: MenuItem[]; - collapseItems: MenuItem[]; -} { - const afterMoreButtonItems = items.filter((item) => item.afterMoreButton); - const regularItems = items.filter((item) => !item.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: MenuItem[] = []; - - let listHeight = allItemsHeight + collapseItemHeight; - let index = listItems.length; - while (listHeight > height) { - if (index === 0) { - break; - } - index--; - - const item = listItems[index]; - if (item.pinned || item.id === COLLAPSE_ITEM_ID || item.afterMoreButton) { - continue; - } - if (item.type === 'divider') { - if (index + 1 < listItems.length && listItems[index + 1]?.type === 'divider') { - listHeight -= getItemHeight(item); - listItems.splice(index, 1); - } - continue; - } - listHeight -= getItemHeight(item); - 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}; -} - -export function isMenuItem(item: CompositeBarItem): item is MenuItem { - return (item as MenuItem)?.id !== undefined; -} diff --git a/src/components/FooterItem/FooterItem.tsx b/src/components/FooterItem/FooterItem.tsx deleted file mode 100644 index 2f0222fb..00000000 --- a/src/components/FooterItem/FooterItem.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -import {Item, ItemProps} from '../CompositeBar/Item/Item'; -import {ASIDE_HEADER_ICON_SIZE} from '../constants'; -import {block} from '../utils/cn'; - -import './FooterItem.scss'; - -const b = block('footer-item'); - -export interface FooterItemProps extends Omit { - compact: boolean; -} - -export const FooterItem: React.FC = ({item, ...props}) => { - return ( - - ); -}; diff --git a/src/components/index.ts b/src/components/index.ts index c987ff31..22d67378 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,10 +1,4 @@ -export {AsideHeader} from './AsideHeader/AsideHeader'; -export type {AsideHeaderProps} from './AsideHeader/types'; -export {AsideHeaderContextProvider, useAsideHeaderContext} from './AsideHeader/AsideHeaderContext'; -export {FooterItem, type FooterItemProps} from './FooterItem/FooterItem'; -export {PageLayout, type PageLayoutProps} from './AsideHeader/components/PageLayout/PageLayout'; -export {PageLayoutAside} from './AsideHeader/components/PageLayout/PageLayoutAside'; -export {AsideFallback} from './AsideHeader/components/PageLayout/AsideFallback'; +export * from './AsideHeader'; export * from './Drawer'; export * from './ActionBar'; export * from './Title'; diff --git a/src/components/types.ts b/src/components/types.ts index dae1f206..de05f028 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -2,8 +2,6 @@ import React, {HTMLAttributeAnchorTarget} from 'react'; import {AlertProps, IconProps, QAProps} from '@gravity-ui/uikit'; -import {ItemProps} from './CompositeBar/Item/Item'; - export type MenuItemType = 'regular' | 'action' | 'divider'; export type OpenModalSubscriber = (open: boolean) => void; @@ -58,8 +56,6 @@ export interface MenuItem extends QAProps { category?: string; } -export type SubheaderMenuItem = Omit; - export interface LogoProps { text: (() => React.ReactNode) | string; className?: string;