From 0e0dfa36cec10f633c587341ed1337b502db8180 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:03:16 +0000 Subject: [PATCH 01/32] chore(deps): update @consta/uikit version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 259dcc3..e8b7599 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@bem-react/classname": "^1.6.0", "@bem-react/classnames": "^1.3.10", "@consta/icons": "^1.1.1", - "@consta/uikit": "^5.22.0", + "@consta/uikit": "^5.26.0", "date-fns": "^2.30.0" }, "config": { From 2d4769018947235ada90e17076fa4d0d6ae82d4e Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:05:18 +0000 Subject: [PATCH 02/32] chore(package.json): add test script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8b7599..9a7600d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "format:svg": "prettier --write '**/*.svg' --parser html", "pre-push": "yarn run tsc-dry-run", "pre-commit": "yarn run lint-staged", - "test": "yarn tsc-dry-run", + "test": "yarn tsc-dry-run && yarn unit", "unit": "jest", "unit:watch": "jest --watch", "unit:clear": "jest --clearCache", From d0c9b5a3081684381af7cecb822f899e59b9b8ae Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:07:05 +0000 Subject: [PATCH 03/32] refactor(Navbar): remove unused NavbarDrawer --- .../Navbar/NavbarItem/NavbarDrawer.tsx | 92 ------------------- 1 file changed, 92 deletions(-) delete mode 100644 src/components/Navbar/NavbarItem/NavbarDrawer.tsx diff --git a/src/components/Navbar/NavbarItem/NavbarDrawer.tsx b/src/components/Navbar/NavbarItem/NavbarDrawer.tsx deleted file mode 100644 index 9d03594..0000000 --- a/src/components/Navbar/NavbarItem/NavbarDrawer.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { getGroups } from '@consta/uikit/__internal__/src/utils/getGroups'; -import { renderHeader } from '@consta/uikit/ListCanary'; -import React, { forwardRef, useMemo } from 'react'; - -import { withDefaultGetters } from '../helpers'; -import { - defaultNavbarPropForm, - defaultNavbarPropSize, - NavbarComponent, - NavbarProps, -} from '../types'; -import { NavbarItem } from '.'; - -const NavbarRender = (props: NavbarProps, ref: React.Ref) => { - const { - items, - onItemClick, - groups: groupsProp, - getItemLabel, - getItemIcon, - getItemRightSide, - getItemAs, - getItemAttributes, - getItemGroupKey, - getItemActive, - getItemRef, - getItemAdditionalClassName, - getGroupKey, - getGroupLabel, - getGroupRightSide, - getGroupAdditionalClassName, - size = defaultNavbarPropSize, - form = defaultNavbarPropForm, - getItemSubMenu, - sortGroup, - className, - getItemStatus, - ...otherProps - } = withDefaultGetters(props); - - const groups = useMemo( - () => getGroups(items, getItemGroupKey, groupsProp, getGroupKey, sortGroup), - [groupsProp, items], - ); - - return ( -
- {groups.map((group, groupIndex) => { - return ( - - {renderHeader( - group.group && getGroupLabel(group.group), - groupIndex === 0, - size, - group.group && getGroupRightSide(group.group), - { pV: 'xs', mH: 'm', mB: '2xs' }, - undefined, - getGroupAdditionalClassName && - group.group && - getGroupAdditionalClassName(group.group), - )} - {group.items.map((item, index) => { - return ( - - - - ); - })} - - ); - })} -
- ); -}; - -export const Navbar = forwardRef(NavbarRender) as NavbarComponent; From ac5554ca170303915787e955673758991236e401 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:09:09 +0000 Subject: [PATCH 04/32] feat(Navbar): add types for controlled subMenu - Added `NavbarPropGetItemSubMenuOpen` - Added `NavbarPropOnItemSubMenuToggle` - Updated `NavbarProps` and `NavbarItemProps` to support controlled sub-menu behavior --- src/components/Navbar/types.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/Navbar/types.ts b/src/components/Navbar/types.ts index 48d7729..708d7e2 100644 --- a/src/components/Navbar/types.ts +++ b/src/components/Navbar/types.ts @@ -7,7 +7,7 @@ import { import { PropsWithHTMLAttributesAndRef } from '@consta/uikit/__internal__/src/utils/types/PropsWithHTMLAttributes'; import { BadgePropStatus } from '@consta/uikit/Badge'; import { TooltipProps } from '@consta/uikit/Tooltip'; -import React from 'react'; +import * as React from 'react'; export const navbarPropSize = ['s', 'm'] as const; export type NavbarPropSize = (typeof navbarPropSize)[number]; @@ -96,6 +96,16 @@ export type NavbarPropSortGroup = ( b: Group, ) => number; +export type NavbarPropGetItemSubMenuOpen = ( + item: ITEM, +) => boolean | undefined; + +export type NavbarPropOnItemSubMenuToggle = ( + item: ITEM, + open: boolean, + params: { e?: React.MouseEvent }, +) => void; + // GROUPS export type NavbarPropGetGroupKey = ( item: GROUP, @@ -151,6 +161,8 @@ export type NavbarProps< getGroupRightSide?: NavbarPropGetGroupRightSide; sortGroup?: NavbarPropSortGroup; getGroupAdditionalClassName?: NavbarPropGetGroupAdditionalClassName; + getItemSubMenuOpen?: NavbarPropGetItemSubMenuOpen; + onItemSubMenuToggle?: NavbarPropOnItemSubMenuToggle; }, HTMLDivElement > & @@ -219,6 +231,8 @@ export type NavbarItemProps = | NavbarPropGetItemAdditionalClassName | undefined; level: number; + getItemSubMenuOpen: NavbarPropGetItemSubMenuOpen | undefined; + onItemSubMenuToggle: NavbarPropOnItemSubMenuToggle | undefined; }, HTMLDivElement >; From 02f4177535f1b92881ce0a0ca30da08246c2b7e4 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:11:08 +0000 Subject: [PATCH 05/32] feat(Navbar): add passing of subMenu props - Pass `getItemSubMenuOpen` and `onItemSubMenuToggle` to `NavbarItem` - Update `withDefaultGetters` --- src/components/Navbar/Navbar.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index cb08fbf..e598983 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -13,7 +13,7 @@ import { NavbarProps, } from './types'; -const cnNavbar = cnCanary('Navbar'); +export const cnNavbar = cnCanary('Navbar'); const NavbarRender = (props: NavbarProps, ref: React.Ref) => { const { @@ -37,6 +37,8 @@ const NavbarRender = (props: NavbarProps, ref: React.Ref) => { form = defaultNavbarPropForm, getItemSubMenu, getItemStatus, + getItemSubMenuOpen, + onItemSubMenuToggle, sortGroup, className, ...otherProps @@ -81,6 +83,8 @@ const NavbarRender = (props: NavbarProps, ref: React.Ref) => { getItemRightSide={getItemRightSide} getItemSubMenu={getItemSubMenu} getItemStatus={getItemStatus} + getItemSubMenuOpen={getItemSubMenuOpen} + onItemSubMenuToggle={onItemSubMenuToggle} form={form} /> From 7cf28f53a79f8dece64e2b48981449260c1f9ca3 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:15:31 +0000 Subject: [PATCH 06/32] feat(Navbar): implement controlled subMenu state in NavbarItem - Add controlled/uncontrolled logic via `getItemSubMenuOpen` and `onItemSubMenuToggle` - Use external state when controlled, preserve local state otherwise - Update click handlers and arrow toggle behavior - Ensure backward compatibility --- .../Navbar/NavbarItem/NavbarItem.tsx | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/components/Navbar/NavbarItem/NavbarItem.tsx b/src/components/Navbar/NavbarItem/NavbarItem.tsx index 4bee540..d3bd02e 100644 --- a/src/components/Navbar/NavbarItem/NavbarItem.tsx +++ b/src/components/Navbar/NavbarItem/NavbarItem.tsx @@ -15,19 +15,19 @@ import { NavbarItemProps, } from '../types'; -const cnNavbarItem = cn('NavbarItem'); +export const cnNavbarItem = cn('NavbarItem'); -const spaceMap = { +export const spaceMap = { m: { pV: 's', pH: 'm', mB: '2xs' }, s: { pV: 'xs', pH: 'm', mB: '2xs' }, } as const; -const mapLevelSpace = { +export const mapLevelSpace = { m: '2xl', s: 'xl', } as const; -const bageSizeMap = { +export const bageSizeMap = { s: 'xs', m: 's', } as const; @@ -51,16 +51,31 @@ const NavbarItemRender = ( onItemClick, className, getItemStatus, + getItemSubMenuOpen, + onItemSubMenuToggle, level = 0, form, } = props; const [open, setOpen] = useFlag(); + const isControlled = !!getItemSubMenuOpen && !!onItemSubMenuToggle; + const isOpen = isControlled ? getItemSubMenuOpen(item) ?? false : open; const subItems = getItemSubMenu?.(item); const rightSide = getItemRightSide?.(item); const active = getItemActive?.(item); const status = getItemStatus?.(item); + const handleToggle = (e: React.MouseEvent) => { + if (subItems?.length) { + const newOpen = !isOpen; + if (isControlled) { + onItemSubMenuToggle(item, newOpen, { e }); + } else { + setOpen.toggle(); + } + } + }; + return ( <> { - subItems?.length && setOpen.toggle(); + handleToggle(e); onItemClick?.(item, { e }); }} leftIcon={getItemIcon?.(item)} @@ -79,7 +94,12 @@ const NavbarItemRender = ( ) : undefined, subItems?.length ? ( - + { + handleToggle(e); + }} + /> ) : undefined, ]} as={getItemAs?.(item)} @@ -96,9 +116,9 @@ const NavbarItemRender = ( ['--navbar-item-level-space' as string]: `var(--space-${mapLevelSpace[size]})`, }} /> - {open && - subItems?.map((item, index) => ( - + {isOpen && + subItems?.map((subItem, index) => ( + ))} ); From d852ee6a81d010a1e5412b520a10ddc5d343c2ae Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:25:13 +0000 Subject: [PATCH 07/32] chore(NavbarArrow): add tests for arrow component --- .../__tests__/NavbarArrow.test.tsx | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/components/Navbar/NavbarArrow/__tests__/NavbarArrow.test.tsx diff --git a/src/components/Navbar/NavbarArrow/__tests__/NavbarArrow.test.tsx b/src/components/Navbar/NavbarArrow/__tests__/NavbarArrow.test.tsx new file mode 100644 index 0000000..7ce2ac3 --- /dev/null +++ b/src/components/Navbar/NavbarArrow/__tests__/NavbarArrow.test.tsx @@ -0,0 +1,159 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { NavbarArrow } from '../NavbarArrow'; + +const defaultProps = { + open: false, + onClick: jest.fn(), +}; + +const renderNavbarArrow = (props = {}) => { + return render(); +}; + +describe('Компонент NavbarArrow', () => { + describe('Базовый рендеринг', () => { + it('должен рендериться без ошибок', () => { + expect(() => renderNavbarArrow()).not.toThrow(); + }); + + it('должен отображать кнопку', () => { + renderNavbarArrow(); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('должен отображать иконку стрелки', () => { + renderNavbarArrow(); + const icon = document.querySelector('.IconArrowDown'); + expect(icon).toBeInTheDocument(); + }); + }); + + describe('Пропсы и состояние', () => { + it('должен применять правильные пропсы к Button', () => { + renderNavbarArrow(); + + const button = screen.getByRole('button'); + + expect(button).toHaveClass('Button_size_xs'); + expect(button).toHaveClass('Button_view_clear'); + expect(button).toHaveClass('Button_onlyIcon'); + expect(button).toHaveAttribute('tabindex', '-1'); + }); + + it('должен передавать open в AnimateIconSwitcherProvider', () => { + const { rerender } = renderNavbarArrow({ open: false }); + + let iconContainer = document.querySelector('.icons--AnimateIconBase'); + expect(iconContainer).toHaveStyle( + '--animate-icon-direction: rotate(0deg)', + ); + + rerender(); + iconContainer = document.querySelector('.icons--AnimateIconBase'); + expect(iconContainer).toHaveStyle( + '--animate-icon-direction: rotate(180deg)', + ); + }); + + it('должен корректно работать с разными состояниями open', () => { + const { rerender } = renderNavbarArrow({ open: false }); + + expect(document.querySelector('.icons--AnimateIconBase')).toHaveStyle( + '--animate-icon-direction: rotate(0deg)', + ); + + rerender(); + expect(document.querySelector('.icons--AnimateIconBase')).toHaveStyle( + '--animate-icon-direction: rotate(180deg)', + ); + + rerender(); + expect(document.querySelector('.icons--AnimateIconBase')).toHaveStyle( + '--animate-icon-direction: rotate(0deg)', + ); + }); + }); + + describe('Взаимодействие', () => { + it('должен вызывать onClick при клике на кнопку', () => { + const onClick = jest.fn(); + renderNavbarArrow({ onClick }); + + fireEvent.click(screen.getByRole('button')); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('должен корректно обрабатывать multiple clicks', () => { + const onClick = jest.fn(); + renderNavbarArrow({ onClick }); + + const button = screen.getByRole('button'); + + fireEvent.click(button); + fireEvent.click(button); + fireEvent.click(button); + + expect(onClick).toHaveBeenCalledTimes(3); + }); + }); + + describe('Визуальное состояние', () => { + it('должен иметь правильные стили для размера xs', () => { + renderNavbarArrow(); + + const button = screen.getByRole('button'); + const icon = document.querySelector('.icons--Icon'); + + expect(button).toHaveClass('Button_size_xs'); + expect(icon).toHaveClass('icons--Icon_size_xs'); + }); + + it('должен иметь прозрачный вид (view="clear")', () => { + renderNavbarArrow(); + + const button = screen.getByRole('button'); + expect(button).toHaveClass('Button_view_clear'); + }); + + it('должен быть только с иконкой (onlyIcon)', () => { + renderNavbarArrow(); + + const button = screen.getByRole('button'); + expect(button).toHaveClass('Button_onlyIcon'); + }); + }); + + describe('Accessibility', () => { + it('должен иметь tabIndex -1', () => { + renderNavbarArrow(); + + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('tabindex', '-1'); + }); + + it('должен быть доступен для кликов', () => { + renderNavbarArrow(); + + const button = screen.getByRole('button'); + expect(button).not.toBeDisabled(); + }); + }); + + describe('Интеграция с анимацией', () => { + it('должен использовать AnimateIconSwitcherProvider', () => { + renderNavbarArrow(); + + const provider = document.querySelector('.icons--AnimateIconBase'); + expect(provider).toBeInTheDocument(); + }); + + it('должен использовать HOC withAnimateSwitcherHOC для иконки', () => { + renderNavbarArrow(); + + const arrowIcon = document.querySelector('.IconArrowDown'); + expect(arrowIcon).toBeInTheDocument(); + }); + }); +}); From 376eef669995358ea6491766d8e1d873bc185d46 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:28:29 +0000 Subject: [PATCH 08/32] chore(NavbarRailItem): add tests for rail item component --- .../Navbar/NavbarRailItem/NavbarRailItem.tsx | 4 +- .../__tests__/NavbarRailItem.test.tsx | 287 ++++++++++++++++++ 2 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 src/components/Navbar/NavbarRailItem/__tests__/NavbarRailItem.test.tsx diff --git a/src/components/Navbar/NavbarRailItem/NavbarRailItem.tsx b/src/components/Navbar/NavbarRailItem/NavbarRailItem.tsx index f08ceee..7d38b30 100644 --- a/src/components/Navbar/NavbarRailItem/NavbarRailItem.tsx +++ b/src/components/Navbar/NavbarRailItem/NavbarRailItem.tsx @@ -15,9 +15,9 @@ import { NavbarRailItemProps, } from '../types'; -const cnNavbarItem = cn('NavbarRailItem'); +export const cnNavbarItem = cn('NavbarRailItem'); -const spaceMap = { +export const spaceMap = { m: { pV: 's', pH: 'm', mB: '2xs' }, s: { pV: 'xs', pH: 'm', mB: '2xs' }, } as const; diff --git a/src/components/Navbar/NavbarRailItem/__tests__/NavbarRailItem.test.tsx b/src/components/Navbar/NavbarRailItem/__tests__/NavbarRailItem.test.tsx new file mode 100644 index 0000000..19128ce --- /dev/null +++ b/src/components/Navbar/NavbarRailItem/__tests__/NavbarRailItem.test.tsx @@ -0,0 +1,287 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import * as React from 'react'; + +import { cnNavbarItem, NavbarRailItem, spaceMap } from '../NavbarRailItem'; + +export const createNavbarRailItemProps = (customProps: any = {}) => ({ + size: 'm' as const, + form: 'default' as const, + active: false, + ...customProps, +}); + +const defaultItemProps = { + label: 'Test Item', + icon: () => Icon, +}; + +const activeItem = { + label: 'Active Item', + icon: () => Icon, + active: true, +}; + +const itemWithStatus = { + label: 'Item with Status', + icon: () => Icon, + status: 'error' as const, +}; + +const minimalItem = { + label: 'Minimal Item', + icon: () => Icon, +}; + +const renderComponent = (props: any = {}) => { + const itemProps = createNavbarRailItemProps(props); + return render(); +}; + +const getNavbarRailItemElement = () => { + return document.querySelector(`.${cnNavbarItem()}`) as HTMLElement; +}; + +describe('Компонент NavbarRailItem', () => { + describe('Базовый рендеринг', () => { + it('должен рендериться без ошибок с минимальными пропсами', () => { + expect(() => renderComponent(minimalItem)).not.toThrow(); + }); + + it('должен отображать label и icon', () => { + renderComponent(defaultItemProps); + + expect(screen.getByText('Test Item')).toBeInTheDocument(); + expect(screen.getByTestId('test-icon')).toBeInTheDocument(); + }); + + it('должен корректно рендериться со всеми комбинациями размеров и форм', () => { + const sizes = Object.keys(spaceMap) as (keyof typeof spaceMap)[]; + const forms = ['default', 'round', 'brick'] as const; + + sizes.forEach((size) => { + forms.forEach((form) => { + expect(() => + renderComponent({ ...defaultItemProps, size, form }), + ).not.toThrow(); + }); + }); + }); + }); + + describe('Состояния и статусы', () => { + it('должен применять модификатор active', () => { + renderComponent(activeItem); + const element = getNavbarRailItemElement(); + expect(element).toHaveClass(cnNavbarItem({ active: true })); + }); + + it('должен отображать Badge при наличии статуса', () => { + renderComponent(itemWithStatus); + + const badge = document.querySelector('.Badge'); + expect(badge).toBeInTheDocument(); + }); + + it('не должен отображать Badge если статуса нет', () => { + renderComponent(defaultItemProps); + + const badge = document.querySelector('.Badge'); + expect(badge).not.toBeInTheDocument(); + }); + }); + + describe('Размеры и отступы', () => { + it('должен применять правильные размеры из spaceMap', () => { + const sizes = Object.keys(spaceMap) as (keyof typeof spaceMap)[]; + + sizes.forEach((size) => { + const { unmount } = renderComponent({ ...defaultItemProps, size }); + const element = getNavbarRailItemElement(); + + expect(element).toHaveClass(cnNavbarItem({ size })); + unmount(); + }); + }); + }); + + describe('Структура компонента', () => { + it('должен иметь правильную структуру с иконкой и текстом', () => { + renderComponent(defaultItemProps); + + const iconWrapper = document.querySelector( + `.${cnNavbarItem('IconWrapper')}`, + ); + const label = document.querySelector(`.${cnNavbarItem('Label')}`); + + expect(iconWrapper).toBeInTheDocument(); + expect(label).toBeInTheDocument(); + expect(screen.getByTestId('test-icon')).toBeInTheDocument(); + expect(screen.getByText('Test Item')).toBeInTheDocument(); + }); + + it('должен отображать только иконку если label не передан', () => { + renderComponent({ + icon: () => Icon, + label: undefined, + }); + + expect(screen.getByTestId('icon-only')).toBeInTheDocument(); + expect(screen.queryByText('Test Item')).not.toBeInTheDocument(); + + const label = document.querySelector(`.${cnNavbarItem('Label')}`); + expect(label).not.toBeInTheDocument(); + }); + + it('должен позиционировать Badge внутри IconWrapper', () => { + renderComponent(itemWithStatus); + + const iconWrapper = document.querySelector( + `.${cnNavbarItem('IconWrapper')}`, + ); + const badge = document.querySelector('.Badge') as HTMLElement; + + expect(iconWrapper).toBeInTheDocument(); + expect(badge).toBeInTheDocument(); + expect(iconWrapper).toContainElement(badge); + }); + }); + + describe('Кастомизация', () => { + it('должен применять кастомный className', () => { + renderComponent({ + ...defaultItemProps, + className: 'custom-class', + }); + + const element = getNavbarRailItemElement(); + expect(element).toHaveClass('custom-class'); + }); + + it('должен рендериться как кастомный HTML элемент через prop as', () => { + renderComponent({ + ...defaultItemProps, + as: 'button', + }); + + const element = getNavbarRailItemElement(); + expect(element.tagName.toLowerCase()).toBe('button'); + }); + + it('должен передавать все дополнительные HTML атрибуты', () => { + renderComponent({ + ...defaultItemProps, + 'data-testid': 'rail-item', + 'title': 'Custom title', + 'id': 'test-id', + }); + + const element = getNavbarRailItemElement(); + expect(element).toHaveAttribute('data-testid', 'rail-item'); + expect(element).toHaveAttribute('title', 'Custom title'); + expect(element).toHaveAttribute('id', 'test-id'); + }); + }); + + describe('Обработка событий', () => { + it('должен обрабатывать клики', () => { + const onClick = jest.fn(); + renderComponent({ + ...defaultItemProps, + as: 'button', + onClick, + }); + + fireEvent.click(screen.getByText('Test Item')); + expect(onClick).toHaveBeenCalled(); + }); + + it('должен обрабатывать hover события (для tooltip)', () => { + renderComponent(defaultItemProps); + + const element = getNavbarRailItemElement(); + fireEvent.mouseEnter(element); + fireEvent.mouseLeave(element); + + expect(screen.getByText('Test Item')).toBeInTheDocument(); + }); + }); + + describe('Edge cases', () => { + it('не должен работать с пустой строкой в label', () => { + renderComponent({ + icon: () => Icon, + label: '', + }); + + expect(screen.getByTestId('empty-label-icon')).toBeInTheDocument(); + const label = document.querySelector(`.${cnNavbarItem('Label')}`); + expect(label).not.toBeInTheDocument(); + }); + + it('должен работать когда переданы только обязательные пропсы', () => { + expect(() => renderComponent({})).not.toThrow(); + + const element = getNavbarRailItemElement(); + expect(element).toBeInTheDocument(); + }); + + it('должен корректно обрабатывать null в icon', () => { + renderComponent({ + label: 'Item with Null Icon', + icon: null, + }); + + expect(screen.getByText('Item with Null Icon')).toBeInTheDocument(); + expect(screen.queryByTestId('test-icon')).not.toBeInTheDocument(); + }); + + it('должен обрабатывать все возможные комбинации пропсов', () => { + const complexProps = { + 'label': 'Complex Item', + 'icon': () => Icon, + 'status': 'warning' as const, + 'active': true, + 'size': 's' as const, + 'form': 'brick' as const, + 'as': 'span' as const, + 'className': 'complex-class', + 'data-complex': 'true', + }; + + expect(() => renderComponent(complexProps)).not.toThrow(); + + const element = getNavbarRailItemElement(); + expect(element).toHaveClass( + cnNavbarItem({ + size: 's', + form: 'brick', + active: true, + }), + ); + expect(element).toHaveClass('complex-class'); + expect(element).toHaveAttribute('data-complex', 'true'); + expect(element.tagName.toLowerCase()).toBe('span'); + }); + }); + + describe('Специфика Rail версии', () => { + it('должен быть компактным (иконка + подпись)', () => { + renderComponent(defaultItemProps); + + const iconWrapper = document.querySelector( + `.${cnNavbarItem('IconWrapper')}`, + ); + const label = document.querySelector(`.${cnNavbarItem('Label')}`); + + expect(iconWrapper).toBeInTheDocument(); + expect(label).toBeInTheDocument(); + }); + + it('не должен поддерживать subMenu (в отличие от обычного NavbarItem)', () => { + renderComponent(defaultItemProps); + + const arrows = document.querySelectorAll('[class*="Arrow"]'); + expect(arrows.length).toBe(0); + }); + }); +}); From e2f03c574fdf084bf3c83c30946561423fed28f4 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:30:30 +0000 Subject: [PATCH 09/32] chore(NavbarRail): add tests for rail container component --- .../NavbarRail/__tests__/NavbarRail.test.tsx | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 src/components/Navbar/NavbarRail/__tests__/NavbarRail.test.tsx diff --git a/src/components/Navbar/NavbarRail/__tests__/NavbarRail.test.tsx b/src/components/Navbar/NavbarRail/__tests__/NavbarRail.test.tsx new file mode 100644 index 0000000..f0723b2 --- /dev/null +++ b/src/components/Navbar/NavbarRail/__tests__/NavbarRail.test.tsx @@ -0,0 +1,279 @@ +import { IconComponent } from '@consta/icons/Icon'; +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { cnNavbarItem } from '../../NavbarRailItem'; +import { DefaultNavbarRailItem } from '../../types'; +import { NavbarRail } from '../NavbarRail'; + +const simpleItems: DefaultNavbarRailItem[] = [ + { label: 'Item 1', icon: () => Icon1 }, + { label: 'Item 2', icon: () => Icon2 }, + { label: 'Item 3', icon: () => Icon3 }, +]; + +const itemsWithStatus: DefaultNavbarRailItem[] = [ + { + label: 'Item with Status', + icon: () => Icon, + status: 'error' as const, + }, + { label: 'Active Item', icon: () => Icon, active: true }, +]; + +const customItems = [ + { id: 1, title: 'Custom Item', iconName: 'home', isActive: true }, +]; + +describe('Компонент NavbarRail', () => { + describe('Базовый рендеринг', () => { + it('должен рендериться без ошибок с минимальными пропсами', () => { + expect(() => render()).not.toThrow(); + }); + + it('должен отображать все переданные элементы', () => { + render(); + + expect(screen.getByText('Item 1')).toBeInTheDocument(); + expect(screen.getByText('Item 2')).toBeInTheDocument(); + expect(screen.getByText('Item 3')).toBeInTheDocument(); + }); + + it('должен применять кастомный className', () => { + const { container } = render( + , + ); + + const railElement = container.firstChild as HTMLElement; + expect(railElement).toHaveClass('custom-rail'); + }); + }); + + describe('Размеры и формы', () => { + it('должен работать с разными размерами', () => { + const sizes = ['s', 'm'] as const; + + sizes.forEach((size) => { + const { container, unmount } = render( + , + ); + + const railItems = container.querySelectorAll(`.${cnNavbarItem()}`); + expect(railItems.length).toBe(3); + unmount(); + }); + }); + + it('должен работать с разными формами', () => { + const forms = ['default', 'round', 'brick'] as const; + + forms.forEach((form) => { + const { container, unmount } = render( + , + ); + + const railItems = container.querySelectorAll(`.${cnNavbarItem()}`); + expect(railItems.length).toBe(3); + unmount(); + }); + }); + }); + + describe('Визуальные элементы', () => { + it('должен отображать Badge при наличии статуса', () => { + render(); + + const badges = document.querySelectorAll('.Badge'); + expect(badges.length).toBeGreaterThan(0); + }); + }); + + describe('Взаимодействие', () => { + it('должен вызывать onItemClick при клике на элемент', () => { + const onItemClick = jest.fn(); + render(); + + fireEvent.click(screen.getByText('Item 1')); + expect(onItemClick).toHaveBeenCalledWith(simpleItems[0], { + e: expect.any(Object), + }); + }); + + it('должен корректно обрабатывать клики по разным элементам', () => { + const onItemClick = jest.fn(); + render(); + + fireEvent.click(screen.getByText('Item 1')); + fireEvent.click(screen.getByText('Item 2')); + fireEvent.click(screen.getByText('Item 3')); + + expect(onItemClick).toHaveBeenCalledTimes(3); + expect(onItemClick).toHaveBeenCalledWith(simpleItems[0], { + e: expect.any(Object), + }); + expect(onItemClick).toHaveBeenCalledWith(simpleItems[1], { + e: expect.any(Object), + }); + expect(onItemClick).toHaveBeenCalledWith(simpleItems[2], { + e: expect.any(Object), + }); + }); + }); + + describe('Кастомизация', () => { + it('должен применять кастомные геттеры для элементов', () => { + const getItemLabel = jest.fn((item: any) => item.title); + const getItemIcon = jest.fn((item: any) => () => CustomIcon); + const getItemActive = jest.fn((item: any) => item.isActive); + + render( + , + ); + + expect(screen.getByText('Custom Item')).toBeInTheDocument(); + expect(getItemLabel).toHaveBeenCalledWith(customItems[0]); + expect(getItemIcon).toHaveBeenCalledWith(customItems[0]); + expect(getItemActive).toHaveBeenCalledWith(customItems[0]); + }); + + it('должен применять кастомные атрибуты для элементов', () => { + const getItemAttributes = jest.fn().mockReturnValue({ + 'data-custom': 'value', + 'title': 'Custom Title', + }); + + render( + , + ); + + expect(getItemAttributes).toHaveBeenCalledWith(simpleItems[0]); + }); + + it('должен работать с кастомным элементом через getItemAs', () => { + const getItemAs = jest.fn().mockReturnValue('button'); + + render(); + + expect(getItemAs).toHaveBeenCalledWith(simpleItems[0]); + }); + }); + + describe('Ref передача', () => { + it('должен корректно передавать ref на контейнер', () => { + const ref = React.createRef(); + render(); + + expect(ref.current).toBeInstanceOf(HTMLDivElement); + }); + + it('должен работать с getItemRef для элементов', () => { + const mockRef = React.createRef(); + const getItemRef = jest.fn().mockReturnValue(mockRef); + + render(); + + expect(getItemRef).toHaveBeenCalledWith(simpleItems[0]); + }); + }); + + describe('Edge cases', () => { + const emptyItems: DefaultNavbarRailItem[] = []; + + it('должен работать с пустым массивом items', () => { + expect(() => render()).not.toThrow(); + + const { container } = render(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('должен корректно обрабатывать отсутствие необязательных геттеров', () => { + const minimalItems = [ + { label: 'Minimal Item', icon: () => Icon }, + ]; + + render(); + + expect(screen.getByText('Minimal Item')).toBeInTheDocument(); + }); + + it('должен работать с кастомными типами данных', () => { + type CustomRailItem = { + code: string; + displayName: string; + iconComponent: IconComponent; + isSelected: boolean; + }; + + const customItems: CustomRailItem[] = [ + { + code: 'home', + displayName: 'Home Page', + iconComponent: () => ( + IconComponent + ), + isSelected: true, + }, + ]; + + const { getByText } = render( + + items={customItems} + getItemLabel={(item) => item.displayName} + getItemIcon={(item) => item.iconComponent} + getItemActive={(item) => item.isSelected} + />, + ); + + expect(getByText('Home Page')).toBeInTheDocument(); + }); + }); + + describe('Комплексные сценарии', () => { + it('должен корректно работать с комбинацией всех фич', () => { + const complexItems: DefaultNavbarRailItem[] = [ + { + label: 'Active Item', + icon: () => Icon, + active: true, + status: 'success' as const, + tooltip: 'Active item tooltip', + }, + { + label: 'Normal Item', + icon: () => Icon, + tooltip: 'Normal item tooltip', + }, + ]; + + const onItemClick = jest.fn(); + + render( + , + ); + + expect(screen.getByText('Active Item')).toBeInTheDocument(); + expect(screen.getByText('Normal Item')).toBeInTheDocument(); + + const badges = document.querySelectorAll('.Badge'); + expect(badges.length).toBeGreaterThan(0); + + fireEvent.click(screen.getByText('Active Item')); + expect(onItemClick).toHaveBeenCalledWith(complexItems[0], { + e: expect.any(Object), + }); + }); + }); +}); From 9829a40e03daad910b346acff5f41cfdb07a83c4 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:36:13 +0000 Subject: [PATCH 10/32] chore(NavbarItem): add tests for NavbarItem component --- .../NavbarItem/__tests__/NavbarItem.test.tsx | 1071 +++++++++++++++++ 1 file changed, 1071 insertions(+) create mode 100644 src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx diff --git a/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx b/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx new file mode 100644 index 0000000..7043aec --- /dev/null +++ b/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx @@ -0,0 +1,1071 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import * as React from 'react'; + +import { withDefaultGetters } from '../../helpers'; +import { DefaultNavbarItem } from '../../types'; +import { + cnNavbarItem, + mapLevelSpace, + NavbarItem, + spaceMap, +} from '../NavbarItem'; + +export const createNavbarItemProps = (customProps: any = {}) => { + const baseGetters = withDefaultGetters({ + items: [], + ...customProps, + }); + + return { + size: 'm' as const, + form: 'default' as const, + level: 0, + onItemClick: jest.fn(), + getItemLabel: baseGetters.getItemLabel, + getItemActive: baseGetters.getItemActive, + getItemIcon: baseGetters.getItemIcon, + getItemRightSide: baseGetters.getItemRightSide, + getItemStatus: baseGetters.getItemStatus, + getItemAs: baseGetters.getItemAs, + getItemAttributes: baseGetters.getItemAttributes, + getItemRef: baseGetters.getItemRef, + getItemSubMenu: baseGetters.getItemSubMenu, + getItemAdditionalClassName: baseGetters.getItemAdditionalClassName, + ...customProps, + }; +}; + +const defaultItem: DefaultNavbarItem = { + label: 'Test Item', +}; + +const itemWithSubMenu: DefaultNavbarItem = { + label: 'Parent Item', + active: false, + subMenu: [ + { label: 'Sub Item 1', active: false }, + { label: 'Sub Item 2', active: true }, + ], +}; + +const renderComponent = (props: any = {}) => { + const itemProps = createNavbarItemProps(props); + return render(); +}; + +const getNavbarItemElement = (text: string) => { + const textElement = screen.getByText(text); + return textElement.closest(`.${cnNavbarItem()}`) as HTMLElement; +}; + +describe('Компонент NavbarItem', () => { + describe('Базовый рендеринг', () => { + it('должен рендериться без ошибок', () => { + expect(() => renderComponent({ item: defaultItem })).not.toThrow(); + }); + + it('должен отображать label элемента', () => { + renderComponent({ item: defaultItem }); + expect(screen.getByText('Test Item')).toBeInTheDocument(); + }); + + it('должен корректно рендериться со всеми комбинациями размеров и форм', () => { + const sizes = Object.keys(spaceMap) as (keyof typeof spaceMap)[]; + const forms = ['default', 'round', 'brick'] as const; + + sizes.forEach((size) => { + forms.forEach((form) => { + expect(() => + renderComponent({ item: defaultItem, size, form }), + ).not.toThrow(); + }); + }); + }); + }); + + describe('Визуальные элементы', () => { + it('должен отображать иконку если она есть', () => { + const itemWithIcon: DefaultNavbarItem = { + label: 'Item with Icon', + active: true, + icon: () => Icon, + }; + + renderComponent({ item: itemWithIcon }); + expect(screen.getByTestId('test-icon')).toBeInTheDocument(); + }); + + it('должен отображать rightSide если он есть', () => { + const itemWithRightSide: DefaultNavbarItem = { + label: 'Item with Right Side', + rightSide: RightSideText, + }; + renderComponent({ item: itemWithRightSide }); + expect(screen.getByTestId('test-right-side')).toBeInTheDocument(); + }); + + it('должен отображать все элементы из массива rightSide', () => { + const itemWithMultipleRightSide: DefaultNavbarItem = { + label: 'Item with Multiple Right Side', + rightSide: [ + + Right1 + , + + Right2 + , + + Right3 + , + ], + }; + renderComponent({ item: itemWithMultipleRightSide }); + + expect(screen.getByTestId('right-1')).toBeInTheDocument(); + expect(screen.getByTestId('right-2')).toBeInTheDocument(); + expect(screen.getByTestId('right-3')).toBeInTheDocument(); + }); + + it('должен отображать Badge при наличии статуса', () => { + const itemWithStatus: DefaultNavbarItem = { + label: 'Item with Status', + status: 'error' as const, + }; + + renderComponent({ item: itemWithStatus }); + const badge = document.querySelector('.Badge'); + expect(badge).toBeInTheDocument(); + }); + + it('не должен отображать Badge если статуса нет', () => { + renderComponent({ item: defaultItem }); + const badge = document.querySelector('.Badge'); + expect(badge).not.toBeInTheDocument(); + }); + + it('должен комбинировать rightSide с Badge и subMeny', () => { + const itemWithEverything: DefaultNavbarItem = { + label: 'Item with Everything', + rightSide: [ + + Right1 + , + + Right2 + , + + Right3 + , + ], + status: 'success' as const, + subMenu: [{ label: 'Sub Item 1' }, { label: 'Sub Item 2' }], + }; + + renderComponent({ item: itemWithEverything }); + + expect(screen.getByTestId('right-1')).toBeInTheDocument(); + expect(screen.getByTestId('right-2')).toBeInTheDocument(); + expect(screen.getByTestId('right-3')).toBeInTheDocument(); + expect(document.querySelector('.Badge')).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('должен корректно обрабатывать пустой массив rightSide', () => { + const itemWithEmptyRightSide: DefaultNavbarItem = { + label: 'Item with Empty Right Side', + rightSide: [], + status: 'error' as const, + subMenu: [{ label: 'Sub Item' }], + }; + + renderComponent({ item: itemWithEmptyRightSide }); + + expect(document.querySelector('.Badge')).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeInTheDocument(); + + expect( + screen.getByText('Item with Empty Right Side'), + ).toBeInTheDocument(); + }); + }); + + describe('БЭМ классы и стили', () => { + it('должен применять базовый класс NavbarItem', () => { + renderComponent({ item: defaultItem }); + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveClass(cnNavbarItem()); + }); + + it('должен применять модификатор active', () => { + const activeItem = { ...defaultItem, active: true }; + renderComponent({ item: activeItem }); + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveClass(cnNavbarItem({ active: true })); + }); + + it('должен применять модификаторы формы', () => { + const forms = ['default', 'round', 'brick'] as const; + + forms.forEach((form) => { + const { unmount } = renderComponent({ item: defaultItem, form }); + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveClass(cnNavbarItem({ form })); + unmount(); + }); + }); + + it('должен применять правильные CSS переменные для разных размеров', () => { + const sizes = Object.keys(spaceMap) as (keyof typeof spaceMap)[]; + + sizes.forEach((size) => { + const { unmount } = renderComponent({ item: defaultItem, size }); + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveStyle({ + '--navbar-item-ph': `var(--space-${spaceMap[size].pH})`, + '--navbar-item-level-space': `var(--space-${mapLevelSpace[size]})`, + }); + unmount(); + }); + }); + + it('должен применять правильные уровни вложенности', () => { + const levels = [0, 1, 2, 3]; + + levels.forEach((level) => { + const { unmount } = renderComponent({ item: defaultItem, level }); + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveStyle({ + '--navbar-item-level': level.toString(), + }); + unmount(); + }); + }); + + it('должен корректно передавать размер для иконки соответствующий размеру Navbar', () => { + const itemWithIcon: DefaultNavbarItem = { + label: 'Item with Icon', + icon: () => Icon, + }; + + const sizes = ['s', 'm'] as const; + + sizes.forEach((size) => { + const { unmount } = renderComponent({ + item: itemWithIcon, + size, + }); + + expect(screen.getByTestId('test-icon')).toBeInTheDocument(); + expect(screen.getByText('Item with Icon')).toBeInTheDocument(); + + const element = getNavbarItemElement('Item with Icon'); + expect(element).toHaveStyle({ + '--navbar-item-ph': `var(--space-${spaceMap[size].pH})`, + '--navbar-item-level-space': `var(--space-${mapLevelSpace[size]})`, + }); + + unmount(); + }); + }); + }); + + describe('Взаимодействие', () => { + describe('SubMenu функциональность неконтролируемое состояние', () => { + it('должен вызывать onItemClick при клике', () => { + const onItemClick = jest.fn(); + renderComponent({ item: defaultItem, onItemClick }); + + fireEvent.click(screen.getByText('Test Item')); + expect(onItemClick).toHaveBeenCalledWith(defaultItem, { + e: expect.any(Object), + }); + }); + + it('должен отображать стрелку если есть subMenu', () => { + renderComponent({ item: itemWithSubMenu }); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('не должен отображать стрелку если нет subMenu', () => { + renderComponent({ item: defaultItem }); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('должен открывать subMenu при клике на стрелку', () => { + renderComponent({ item: itemWithSubMenu }); + + fireEvent.click(screen.getByRole('button')); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + expect(screen.getByText('Sub Item 2')).toBeInTheDocument(); + }); + + it('должен открывать subMenu при клике на сам элемент если есть subMenu', () => { + renderComponent({ item: itemWithSubMenu }); + + fireEvent.click(screen.getByText('Parent Item')); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + }); + + it('должен закрывать subMenu при повторном клике на элемент', () => { + renderComponent({ item: itemWithSubMenu }); + + fireEvent.click(screen.getByText('Parent Item')); + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Parent Item')); + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + }); + + it('должен закрывать subMenu при повторном клике на стрелку', () => { + renderComponent({ item: itemWithSubMenu }); + + fireEvent.click(screen.getByRole('button')); + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button')); + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + }); + }); + + describe('SubMenu функциональность контролируемое состояние', () => { + it('должен вызывать onItemClick даже в контролируемом режиме', () => { + const onItemClick = jest.fn(); + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: itemWithSubMenu, + onItemClick, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + fireEvent.click(screen.getByText('Parent Item')); + + expect(onItemClick).toHaveBeenCalledWith(itemWithSubMenu, { + e: expect.any(Object), + }); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemWithSubMenu, + true, + { e: expect.any(Object) }, + ); + }); + + it('должен отображать стрелку если есть subMenu в контролируемом режиме', () => { + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: itemWithSubMenu, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('должен использовать переданное значение открытия извне', () => { + const getItemSubMenuOpen = jest.fn().mockReturnValue(true); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: itemWithSubMenu, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + expect(screen.getByText('Sub Item 2')).toBeInTheDocument(); + }); + + it('должен использовать переданное значение закрытия извне', () => { + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: itemWithSubMenu, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Sub Item 2')).not.toBeInTheDocument(); + }); + + it('должен вызывать onItemSubMenuToggle при клике, но не менять состояние самостоятельно', () => { + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + const onItemSubMenuToggle = jest.fn(); + + const { rerender } = renderComponent({ + item: itemWithSubMenu, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('Parent Item')); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemWithSubMenu, + true, + { e: expect.any(Object) }, + ); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + + getItemSubMenuOpen.mockReturnValue(true); + rerender( + , + ); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + }); + + it('должен работать полный цикл открытия/закрытия через контролируемое состояние', () => { + let isOpen = false; + const getItemSubMenuOpen = jest.fn(() => isOpen); + const onItemSubMenuToggle = jest.fn((item, newOpen) => { + isOpen = newOpen; + }); + + const { rerender } = renderComponent({ + item: itemWithSubMenu, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('Parent Item')); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemWithSubMenu, + true, + { e: expect.any(Object) }, + ); + + rerender( + , + ); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Parent Item')); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemWithSubMenu, + false, + { e: expect.any(Object) }, + ); + + rerender( + , + ); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + }); + + it('должен работать полный цикл открытия/закрытия через стрелку в контролируемом состоянии', () => { + let isOpen = false; + const getItemSubMenuOpen = jest.fn(() => isOpen); + const onItemSubMenuToggle = jest.fn((item, newOpen) => { + isOpen = newOpen; + }); + + const { rerender } = renderComponent({ + item: itemWithSubMenu, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button')); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemWithSubMenu, + true, + { e: expect.any(Object) }, + ); + + rerender( + , + ); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button')); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemWithSubMenu, + false, + { e: expect.any(Object) }, + ); + + rerender( + , + ); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + }); + + it('должен использовать локальное состояние когда не переданы контролируемые пропсы', () => { + renderComponent({ + item: itemWithSubMenu, + }); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('Parent Item')); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + }); + + it('должен вызывать и onItemClick и onItemSubMenuToggle при клике на элемент с subMenu', () => { + const onItemClick = jest.fn(); + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: itemWithSubMenu, + onItemClick, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + fireEvent.click(screen.getByText('Parent Item')); + + expect(onItemClick).toHaveBeenCalledWith(itemWithSubMenu, { + e: expect.any(Object), + }); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemWithSubMenu, + true, + { e: expect.any(Object) }, + ); + }); + + it('должен вызывать только onItemClick при клике на элемент без subMenu в контролируемом режиме', () => { + const onItemClick = jest.fn(); + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: defaultItem, + onItemClick, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + fireEvent.click(screen.getByText('Test Item')); + + expect(onItemClick).toHaveBeenCalledWith(defaultItem, { + e: expect.any(Object), + }); + expect(onItemSubMenuToggle).not.toHaveBeenCalled(); + }); + }); + + describe('Cascade subMenuu с контролируемым состоянием', () => { + const deepNestedItems: DefaultNavbarItem = { + label: 'Level 1', + subMenu: [ + { + label: 'Level 2', + subMenu: [ + { + label: 'Level 3', + subMenu: [{ label: 'Level 4' }], + }, + ], + }, + ], + }; + + it('не должен отображать вложенные уровни даже с открытым состоянием если родитель закрыт', () => { + const openStates: Record = { + 'Level 1': false, + 'Level 2': true, + 'Level 3': true, + }; + + const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: deepNestedItems, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByText('Level 1')).toBeInTheDocument(); + expect(screen.queryByText('Level 2')).not.toBeInTheDocument(); + expect(screen.queryByText('Level 3')).not.toBeInTheDocument(); + expect(screen.queryByText('Level 4')).not.toBeInTheDocument(); + }); + + it('должен отображать все уровни когда все родители открыты в контролируемом режиме', () => { + const openStates: Record = { + 'Level 1': true, + 'Level 2': true, + 'Level 3': true, + }; + + const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: deepNestedItems, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByText('Level 1')).toBeInTheDocument(); + expect(screen.getByText('Level 2')).toBeInTheDocument(); + expect(screen.getByText('Level 3')).toBeInTheDocument(); + expect(screen.getByText('Level 4')).toBeInTheDocument(); + }); + + it('должен скрывать все вложенные уровни при закрытии родителя в контролируемом режиме', () => { + const openStates: Record = { + 'Level 1': true, + 'Level 2': true, + 'Level 3': true, + }; + + const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); + const onItemSubMenuToggle = jest.fn((item, newOpen) => { + openStates[item.label] = newOpen; + }); + + const { rerender } = renderComponent({ + item: deepNestedItems, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByText('Level 2')).toBeInTheDocument(); + expect(screen.getByText('Level 3')).toBeInTheDocument(); + expect(screen.getByText('Level 4')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Level 1')); + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + deepNestedItems, + false, + { e: expect.any(Object) }, + ); + + expect(openStates['Level 1']).toBe(false); + rerender( + , + ); + + expect(screen.queryByText('Level 2')).not.toBeInTheDocument(); + expect(screen.queryByText('Level 3')).not.toBeInTheDocument(); + expect(screen.queryByText('Level 4')).not.toBeInTheDocument(); + + expect(openStates['Level 2']).toBe(true); + expect(openStates['Level 3']).toBe(true); + }); + + it('должен показывать вложенные уровни только когда все родительские цепочки открыты', () => { + const openStates: Record = { + 'Level 1': true, + 'Level 2': false, + 'Level 3': true, + }; + + const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: deepNestedItems, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByText('Level 1')).toBeInTheDocument(); + expect(screen.getByText('Level 2')).toBeInTheDocument(); + + expect(screen.queryByText('Level 3')).not.toBeInTheDocument(); + expect(screen.queryByText('Level 4')).not.toBeInTheDocument(); + }); + + it('должен работать с частично открытой цепочкой в контролируемом режиме', () => { + const openStates: Record = { + 'Level 1': true, + 'Level 2': true, + 'Level 3': false, + }; + + const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: deepNestedItems, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByText('Level 1')).toBeInTheDocument(); + expect(screen.getByText('Level 2')).toBeInTheDocument(); + expect(screen.getByText('Level 3')).toBeInTheDocument(); + + expect(screen.queryByText('Level 4')).not.toBeInTheDocument(); + }); + }); + }); + + describe('Кастомизация', () => { + it('должен применять кастомный класс из getItemAdditionalClassName', () => { + const getItemAdditionalClassName = jest + .fn() + .mockReturnValue('custom-class'); + const itemWithCustomClassName: DefaultNavbarItem = { + label: 'Item with Custom Class', + }; + + renderComponent({ + item: itemWithCustomClassName, + getItemAdditionalClassName, + }); + + const element = getNavbarItemElement('Item with Custom Class'); + expect(element).toHaveClass('custom-class'); + expect(getItemAdditionalClassName).toHaveBeenCalledWith( + itemWithCustomClassName, + ); + }); + + it('должен комбинировать кастомный класс с БЭМ классами', () => { + const getItemAdditionalClassName = jest + .fn() + .mockReturnValue('custom-class'); + renderComponent({ + item: defaultItem, + getItemAdditionalClassName, + form: 'round', + }); + + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveClass(cnNavbarItem({ form: 'round' })); + expect(element).toHaveClass('custom-class'); + }); + + it('должен применять кастомные атрибуты из getItemAttributes', () => { + const getItemAttributes = jest.fn().mockReturnValue({ + 'data-test': 'custom-attr', + 'title': 'Custom Title', + }); + + renderComponent({ + item: defaultItem, + getItemAttributes, + }); + + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveAttribute('data-test', 'custom-attr'); + expect(element).toHaveAttribute('title', 'Custom Title'); + }); + }); + + describe('Комбинации состояний', () => { + it('должен корректно работать с активным элементом в форме round', () => { + const activeItem = { ...defaultItem, active: true }; + renderComponent({ item: activeItem, form: 'round' }); + + const element = getNavbarItemElement('Test Item'); + expect(element).toHaveClass( + cnNavbarItem({ active: true, form: 'round' }), + ); + }); + + it('должен корректно работать с subMenu и активным состоянием', () => { + const activeItemWithSubMenu = { ...itemWithSubMenu, active: true }; + renderComponent({ item: activeItemWithSubMenu }); + + const element = getNavbarItemElement('Parent Item'); + expect(element).toHaveClass(cnNavbarItem({ active: true })); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + }); + + describe('Кастомные геттеры и структуры данных', () => { + type CustomNavbarItem = { + id: number; + name: string; + isActive: boolean; + iconName: string; + badge: string; + children: CustomNavbarItem[]; + customClass: string; + htmlAttributes: Record; + }; + + const customItem: CustomNavbarItem = { + id: 1, + name: 'Custom Item', + isActive: true, + iconName: 'home', + badge: 'warning', + children: [ + { + id: 2, + name: 'Child Item', + isActive: false, + iconName: 'child', + badge: 'success', + children: [], + customClass: 'child-class', + htmlAttributes: {}, + }, + ], + customClass: 'parent-class', + htmlAttributes: { 'data-custom': 'value', 'title': 'Custom Title' }, + }; + + it('должен работать с кастомными геттерами для всех полей', () => { + const onItemClick = jest.fn(); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: customItem, + + getItemLabel: (item: CustomNavbarItem) => item.name, + getItemActive: (item: CustomNavbarItem) => item.isActive, + getItemIcon: (item: CustomNavbarItem) => () => + {item.iconName}, + getItemStatus: (item: CustomNavbarItem) => item.badge as any, + getItemSubMenu: (item: CustomNavbarItem) => item.children, + getItemAdditionalClassName: (item: CustomNavbarItem) => + item.customClass, + getItemAttributes: (item: CustomNavbarItem) => item.htmlAttributes, + + onItemClick, + onItemSubMenuToggle, + }); + + expect(screen.getByText('Custom Item')).toBeInTheDocument(); + expect(screen.getByTestId('icon-home')).toBeInTheDocument(); + + const element = getNavbarItemElement('Custom Item'); + expect(element).toHaveClass('parent-class'); + expect(element).toHaveAttribute('data-custom', 'value'); + expect(element).toHaveAttribute('title', 'Custom Title'); + + fireEvent.click(screen.getByText('Custom Item')); + expect(onItemClick).toHaveBeenCalledWith(customItem, { + e: expect.any(Object), + }); + }); + + it('должен работать с кастомными геттерами для subMenu', () => { + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + const onItemSubMenuToggle = jest.fn(); + + renderComponent({ + item: customItem, + getItemLabel: (item: CustomNavbarItem) => item.name, + getItemSubMenu: (item: CustomNavbarItem) => item.children, + getItemSubMenuOpen, + onItemSubMenuToggle, + }); + + expect(screen.getByRole('button')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Custom Item')); + expect(onItemSubMenuToggle).toHaveBeenCalledWith(customItem, true, { + e: expect.any(Object), + }); + }); + + it('должен работать с кастомными геттерами для вложенных элементов', () => { + renderComponent({ + item: customItem, + getItemLabel: (item: CustomNavbarItem) => item.name, + getItemSubMenu: (item: CustomNavbarItem) => item.children, + getItemSubMenuOpen: (item: CustomNavbarItem) => item.isActive, + onItemSubMenuToggle: jest.fn(), + }); + + fireEvent.click(screen.getByText('Custom Item')); + + expect(screen.getByText('Child Item')).toBeInTheDocument(); + }); + + it('должен работать с частично кастомными геттерами', () => { + const mixedItem = { + label: 'Standard Label', + customActive: true, + icon: () => Icon, + }; + + renderComponent({ + item: mixedItem, + getItemActive: (item: any) => item.customActive, + }); + + expect(screen.getByText('Standard Label')).toBeInTheDocument(); + expect(screen.getByTestId('mixed-icon')).toBeInTheDocument(); + + const element = getNavbarItemElement('Standard Label'); + expect(element).toHaveClass(cnNavbarItem({ active: true })); + }); + + it('должен корректно обрабатывать отсутствие необязательных геттеров', () => { + const minimalItem = { + name: 'Minimal Item', + }; + + renderComponent({ + item: minimalItem, + getItemLabel: (item: any) => item.name, + }); + + expect(screen.getByText('Minimal Item')).toBeInTheDocument(); + + const element = getNavbarItemElement('Minimal Item'); + expect(element).toBeInTheDocument(); + }); + + it('должен работать с кастомным rightSide через геттер', () => { + const customItemWithRightSide = { + id: 1, + name: 'Item with Custom Right Side', + rightContent: Custom Right, + }; + + renderComponent({ + item: customItemWithRightSide, + getItemLabel: (item: any) => item.name, + getItemRightSide: (item: any) => item.rightContent, + }); + + expect(screen.getByTestId('custom-right')).toBeInTheDocument(); + expect( + screen.getByText('Item with Custom Right Side'), + ).toBeInTheDocument(); + }); + + it('должен комбинировать кастомные геттеры с контролируемым состоянием', () => { + const customItem = { + id: 1, + title: 'Controlled Item', + expanded: false, + items: [{ id: 2, title: 'Nested Item', expanded: false, items: [] }], + }; + + const { rerender } = renderComponent({ + item: customItem, + getItemLabel: (item: any) => item.title, + getItemSubMenu: (item: any) => item.items, + getItemSubMenuOpen: (item: any) => item.expanded, + onItemSubMenuToggle: jest.fn((item, newOpen) => { + item.expanded = newOpen; + }), + }); + + expect(screen.queryByText('Nested Item')).not.toBeInTheDocument(); + + customItem.expanded = true; + rerender( + item.title, + getItemSubMenu: (item: any) => item.items, + getItemSubMenuOpen: (item: any) => item.expanded, + onItemSubMenuToggle: jest.fn(), + })} + />, + ); + + expect(screen.getByText('Nested Item')).toBeInTheDocument(); + }); + }); + + describe('Edge cases с кастомными геттерами', () => { + it('должен обрабатывать null и undefined значения в кастомных геттерах', () => { + const itemWithNulls = { + name: 'Item with Nulls', + active: null, + icon: undefined, + status: null, + }; + + renderComponent({ + item: itemWithNulls, + getItemLabel: (item: any) => item.name, + getItemActive: (item: any) => item.active, + getItemIcon: (item: any) => item.icon, + getItemStatus: (item: any) => item.status, + }); + + expect(screen.getByText('Item with Nulls')).toBeInTheDocument(); + }); + + it('должен работать с функциями-геттерами которые возвращают сложную логику', () => { + const complexItem = { + data: { + title: 'Complex Item', + metadata: { + isSelected: true, + priority: 'high', + }, + }, + }; + + renderComponent({ + item: complexItem, + getItemLabel: (item: any) => item.data.title, + getItemActive: (item: any) => item.data.metadata.isSelected, + getItemAdditionalClassName: (item: any) => + item.data.metadata.priority === 'high' ? 'high-priority' : '', + getItemAttributes: (item: any) => ({ + 'data-priority': item.data.metadata.priority, + }), + }); + + expect(screen.getByText('Complex Item')).toBeInTheDocument(); + + const element = getNavbarItemElement('Complex Item'); + expect(element).toHaveClass('high-priority'); + expect(element).toHaveAttribute('data-priority', 'high'); + }); + }); +}); From 291adb3ed39f086aa525500a73f5a289911517c8 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:38:01 +0000 Subject: [PATCH 11/32] chore(Navbar): add tests for Navbar component --- .../Navbar/__tests__/Navbar.test.tsx | 450 ++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 src/components/Navbar/__tests__/Navbar.test.tsx diff --git a/src/components/Navbar/__tests__/Navbar.test.tsx b/src/components/Navbar/__tests__/Navbar.test.tsx new file mode 100644 index 0000000..93ed555 --- /dev/null +++ b/src/components/Navbar/__tests__/Navbar.test.tsx @@ -0,0 +1,450 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +import { cnNavbar, Navbar } from '../Navbar'; +import { cnNavbarItem } from '../NavbarItem'; +import { DefaultNavbarGroup, DefaultNavbarItem } from '../types'; + +const simpleItems: DefaultNavbarItem[] = [ + { label: 'Item 1' }, + { label: 'Item 2' }, + { label: 'Item 3' }, +]; + +const groupedItems: DefaultNavbarItem[] = [ + { label: 'Item 1', groupId: 'group1' }, + { label: 'Item 2', groupId: 'group1' }, + { label: 'Item 3', groupId: 'group2' }, + { label: 'Item 4', groupId: 'group2' }, +]; + +const groups: DefaultNavbarGroup[] = [ + { id: 'group1', label: 'Group 1' }, + { id: 'group2', label: 'Group 2' }, +]; + +const itemsWithSubMenu: DefaultNavbarItem[] = [ + { + label: 'Parent Item', + subMenu: [{ label: 'Sub Item 1' }, { label: 'Sub Item 2' }], + }, +]; + +const itemsWithIcons: DefaultNavbarItem[] = [ + { + label: 'Item with Icon', + icon: () => Icon1, + }, + { + label: 'Another Item', + icon: () => Icon2, + }, +]; + +describe('Компонент Navbar', () => { + describe('Базовый рендеринг', () => { + it('должен рендериться без ошибок с минимальными пропсами', () => { + expect(() => render()).not.toThrow(); + }); + + it('должен отображать переданные элементы', () => { + render(); + + expect(screen.getByText('Item 1')).toBeInTheDocument(); + expect(screen.getByText('Item 2')).toBeInTheDocument(); + expect(screen.getByText('Item 3')).toBeInTheDocument(); + }); + + it('должен применять базовый класс Navbar', () => { + const { container } = render(); + + const navbarElement = container.querySelector(`.${cnNavbar()}`); + expect(navbarElement).toBeInTheDocument(); + }); + + it('должен применять кастомный className', () => { + const { container } = render( + , + ); + + const navbarElement = container.querySelector(`.${cnNavbar()}`); + expect(navbarElement).toHaveClass('custom-navbar'); + }); + }); + + describe('Группировка элементов', () => { + it('должен отображать группы элементы по groupId', () => { + render(); + + expect(screen.getByText('Group 1')).toBeInTheDocument(); + expect(screen.getByText('Group 2')).toBeInTheDocument(); + expect(screen.getByText('Item 1')).toBeInTheDocument(); + expect(screen.getByText('Item 3')).toBeInTheDocument(); + }); + + it('должен работать с кастомными getGroupKey и getGroupLabel', () => { + const customGroups = [ + { customId: 'g1', customLabel: 'Custom Group 1' }, + { customId: 'g2', customLabel: 'Custom Group 2' }, + ]; + + const customItems = [ + { label: 'Item 1', groupId: 'g1' }, + { label: 'Item 2', groupId: 'g2' }, + ]; + + render( + group.customId} + getGroupLabel={(group: any) => group.customLabel} + />, + ); + + expect(screen.getByText('Custom Group 1')).toBeInTheDocument(); + expect(screen.getByText('Custom Group 2')).toBeInTheDocument(); + }); + + it('должен корректно работать без групп', () => { + render(); + + expect(screen.getByText('Item 1')).toBeInTheDocument(); + expect(screen.getByText('Item 2')).toBeInTheDocument(); + expect(screen.getByText('Item 3')).toBeInTheDocument(); + }); + + it('должен применять кастомные классы для групп', () => { + const getGroupAdditionalClassName = jest + .fn() + .mockReturnValue('custom-group-class'); + + render( + , + ); + + expect(getGroupAdditionalClassName).toHaveBeenCalledWith(groups[0]); + }); + }); + + describe('Размеры и формы', () => { + it('должен работать с разными размерами', () => { + const sizes = ['s', 'm'] as const; + + sizes.forEach((size) => { + const { container, unmount } = render( + , + ); + + const navbarElement = container.querySelector(`.${cnNavbar()}`); + expect(navbarElement).toBeInTheDocument(); + + const navbarItems = container.querySelectorAll(`.${cnNavbarItem()}`); + expect(navbarItems.length).toBeGreaterThan(0); + + unmount(); + }); + }); + + it('должен работать с разными формами', () => { + const forms = ['default', 'round', 'brick'] as const; + + forms.forEach((form) => { + const { container, unmount } = render( + , + ); + + const navbarElement = container.querySelector(`.${cnNavbar()}`); + expect(navbarElement).toBeInTheDocument(); + + const navbarItems = container.querySelectorAll(`.${cnNavbarItem()}`); + expect(navbarItems.length).toBe(3); + + unmount(); + }); + }); + }); + + describe('Визуальные элементы', () => { + it('должен отображать иконки элементов', () => { + render(); + + expect(screen.getByTestId('icon-1')).toBeInTheDocument(); + expect(screen.getByTestId('icon-2')).toBeInTheDocument(); + }); + + it('должен отображать rightSide элементов', () => { + const itemsWithRightSide: DefaultNavbarItem[] = [ + { + label: 'Item with Right Side', + rightSide: RightSide, + }, + ]; + + render(); + + expect(screen.getByTestId('right-side')).toBeInTheDocument(); + }); + + it('должен отображать Badge при наличии статуса', () => { + const itemsWithStatus: DefaultNavbarItem[] = [ + { label: 'Item with Status', status: 'error' as const }, + ]; + + render(); + + const badge = document.querySelector('.Badge'); + expect(badge).toBeInTheDocument(); + }); + + it('должен отображать стрелки для элементов с subMenu', () => { + render(); + + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + }); + + describe('Взаимодействие', () => { + it('должен вызывать onItemClick при клике на элемент', () => { + const onItemClick = jest.fn(); + render(); + + fireEvent.click(screen.getByText('Item 1')); + expect(onItemClick).toHaveBeenCalledWith(simpleItems[0], { + e: expect.any(Object), + }); + }); + + it('должен передавать subMenu пропсы в NavbarItem при контролируемом состоянии', () => { + const onItemSubMenuToggle = jest.fn(); + const getItemSubMenuOpen = jest.fn().mockReturnValue(false); + + render( + , + ); + + fireEvent.click(screen.getByText('Parent Item')); + + expect(onItemSubMenuToggle).toHaveBeenCalledWith( + itemsWithSubMenu[0], + true, + { e: expect.any(Object) }, + ); + }); + + it('должен открывать subMenu при клике на элемент', () => { + render(); + + expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText('Parent Item')); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + expect(screen.getByText('Sub Item 2')).toBeInTheDocument(); + }); + + it('должен работать с контролируемым subMenu', () => { + const getItemSubMenuOpen = jest.fn().mockReturnValue(true); + const onItemSubMenuToggle = jest.fn(); + + render( + , + ); + + expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + expect(screen.getByText('Sub Item 2')).toBeInTheDocument(); + }); + }); + + describe('Кастомизация', () => { + it('должен применять кастомные геттеры для элементов', () => { + const customItems = [{ id: 1, title: 'Custom Item', isActive: true }]; + + const getItemLabel = jest.fn((item: any) => item.title); + const getItemActive = jest.fn((item: any) => item.isActive); + + render( + , + ); + + expect(screen.getByText('Custom Item')).toBeInTheDocument(); + expect(getItemLabel).toHaveBeenCalledWith(customItems[0]); + expect(getItemActive).toHaveBeenCalledWith(customItems[0]); + }); + + it('должен применять кастомные атрибуты для элементов', () => { + const getItemAttributes = jest.fn().mockReturnValue({ + 'data-custom': 'value', + 'title': 'Custom Title', + }); + + render( + , + ); + + expect(getItemAttributes).toHaveBeenCalledWith(simpleItems[0]); + }); + + it('должен применять кастомные классы для элементов', () => { + const getItemAdditionalClassName = jest + .fn() + .mockReturnValue('custom-item-class'); + + render( + , + ); + + expect(getItemAdditionalClassName).toHaveBeenCalledWith(simpleItems[0]); + }); + }); + + describe('Сортировка групп', () => { + it('должен работать с кастомной сортировкой групп', () => { + const customGroups = [ + { id: 'group1', label: 'Group B' }, + { id: 'group2', label: 'Group A' }, + ]; + + const customItems = [ + { label: 'Item 1', groupId: 'group1' }, + { label: 'Item 2', groupId: 'group2' }, + ]; + + const sortGroup = jest.fn((a: any, b: any) => + a.group.label.localeCompare(b.group.label), + ); + + render( + , + ); + + expect(sortGroup).toHaveBeenCalled(); + }); + }); + + describe('Edge cases', () => { + const emptyItems: DefaultNavbarItem[] = []; + const emptyGroups: DefaultNavbarGroup[] = []; + + it('должен работать с пустым массивом items', () => { + const { container } = render(); + + const navbarElement = container.querySelector(`.${cnNavbar()}`); + expect(navbarElement).toBeInTheDocument(); + }); + + it('должен работать с пустым массивом groups', () => { + render(); + + expect(screen.getByText('Item 1')).toBeInTheDocument(); + expect(screen.getByText('Item 2')).toBeInTheDocument(); + }); + + it('должен корректно обрабатывать элементы без groupId когда есть группы', () => { + const mixedItems: DefaultNavbarItem[] = [ + { label: 'Item with Group', groupId: 'group1' }, + { label: 'Item without Group' }, + ]; + + render(); + + expect(screen.getByText('Item with Group')).toBeInTheDocument(); + expect(screen.getByText('Item without Group')).toBeInTheDocument(); + }); + + it('должен работать с кастомными типами и пустыми массивами', () => { + type CustomItem = { id: string; title: string }; + type CustomGroup = { code: string; name: string }; + + const emptyCustomItems: CustomItem[] = []; + const emptyCustomGroups: CustomGroup[] = []; + + const { container } = render( + + items={emptyCustomItems} + groups={emptyCustomGroups} + getItemLabel={(item) => item.title} + getGroupKey={(group) => group.code} + getGroupLabel={(group) => group.name} + />, + ); + + const navbarElement = container.querySelector(`.${cnNavbar()}`); + expect(navbarElement).toBeInTheDocument(); + }); + }); + + describe('Комплексные сценарии', () => { + it('должен корректно работать с комбинацией всех фич', () => { + const complexItems: DefaultNavbarItem[] = [ + { + label: 'Active Item', + groupId: 'group1', + active: true, + icon: () => Icon, + status: 'success' as const, + subMenu: [{ label: 'Nested Item' }], + }, + { + label: 'Simple Item', + groupId: 'group2', + rightSide: rightSide, + }, + ]; + + const onItemClick = jest.fn(); + const onItemSubMenuToggle = jest.fn(); + + render( + , + ); + + expect(screen.getByText('Group 1')).toBeInTheDocument(); + expect(screen.getByText('Group 2')).toBeInTheDocument(); + + expect(screen.getByText('Active Item')).toBeInTheDocument(); + expect(screen.getByText('Simple Item')).toBeInTheDocument(); + + expect(screen.getByTestId('complex-icon')).toBeInTheDocument(); + expect(screen.getByTestId('complex-right')).toBeInTheDocument(); + expect(document.querySelector('.Badge')).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Active Item')); + expect(onItemClick).toHaveBeenCalledWith(complexItems[0], { + e: expect.any(Object), + }); + expect(onItemClick).toHaveBeenCalled(); + }); + }); +}); From 2cc2221a9518f077da59104d06d8f14ef05f9257 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:40:11 +0000 Subject: [PATCH 12/32] chore(jest.config.js): configure test setup --- jest.config.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 4a921d5..f3fc0d4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,9 +5,16 @@ module.exports = { '\\.css$': '/__mocks__/styleMock.js', '##/(.*)$': '/src/$1', }, - coveragePathIgnorePatterns: ['/node_modules/', '/coverage/', '/types/'], + transform: { + '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', + }, testMatch: ['**/*.test.{ts,tsx}'], modulePathIgnorePatterns: ['/dist/'], + coveragePathIgnorePatterns: ['/node_modules/', '/coverage/', '/types/'], + transformIgnorePatterns: [ + // Транспайлим библиотеки на es-модулях в commonjs-модули + `/node_modules/(?!(@consta)/).+\\.(js|jsx|ts|tsx)`, + ], collectCoverageFrom: ['**/*.{ts,tsx}', '!**/*.stories.tsx'], testEnvironment: 'jsdom', setupFilesAfterEnv: ['./jest.setup.ts'], From 375693ce6255f3f93610a549a4649ba34da6a008 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 31 Oct 2025 13:41:31 +0000 Subject: [PATCH 13/32] docs(Navbar): add description and example for new subMenu props --- .../Navbar/__stand__/Navbar.dev.stand.mdx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index 8f439f2..b40191a 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -115,6 +115,32 @@ export const NavbarOnClickExample = () => ( +## Управление состоянием подменю + +Для управления состоянием подменю (открыто/закрыто) используйте связку getItemSubMenuOpen и onItemSubMenuToggle. Это позволяет контролировать открытие подменю через внешний стейт, включая задание начального состояния. + +```tsx +const [openStates, setOpenStates] = React.useState({ 'Пункт 1': true }); +const menu = [ + { label: 'Пункт 1', subMenu: [{ label: 'Подпункт 1' }] }, + { label: 'Пункт 2', subMenu: [{ label: 'Подпункт 2' }] }, +]; + +const getItemSubMenuOpen = (item) => openStates[item.label] || false; + +const onItemSubMenuToggle = (item, open, { e }) => { + setOpenStates((prev) => ({ ...prev, [item.label]: open })); +}; + +export const NavbarControlledExample = () => ( + +); +``` + ## Свойства Navbar ```tsx @@ -196,6 +222,14 @@ type NavbarPropGetItemStatus = ( ) => BadgePropStatus | undefined; type NavbarPropGetItemSubMenu = (item: ITEM) => ITEM[] | undefined; + +type NavbarPropGetItemSubMenuOpen = (item: ITEM) => boolean | undefined; + +type NavbarPropOnItemSubMenuToggle = ( + item: ITEM, + open: boolean, + params: { e?: React.MouseEvent }, +) => void; ``` | Свойство | Тип | По умолчанию | Описание | @@ -223,6 +257,8 @@ type NavbarPropGetItemSubMenu = (item: ITEM) => ITEM[] | undefined; | `getGroupAdditionalClassName` | `NavbarPropGetGroupAdditionalClassName` | - | Функция для определения дополнительного класса группы | | `className?` | `string` | - | Дополнительный CSS-класс для пунктов меню | | `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | +| `getItemSubMenuOpen?` | `NavbarPropGetItemSubMenuOpen` | - | Функция для определения открыто ли подменю элемента | +| `onItemSubMenuToggle?` | `NavbarPropOnItemSubMenuToggle` | - | Функция для обработки изменения состояния подменю элемента | ## NavbarRail From 0a39bbd31caa2ac6cb3b5b10b6a9efdffa464bba Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Wed, 5 Nov 2025 13:30:59 +0000 Subject: [PATCH 14/32] refactor(Navbar): rename onItemSubMenuToggle to onSubMenuToggle --- src/components/Navbar/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Navbar/types.ts b/src/components/Navbar/types.ts index 708d7e2..2e36891 100644 --- a/src/components/Navbar/types.ts +++ b/src/components/Navbar/types.ts @@ -100,7 +100,7 @@ export type NavbarPropGetItemSubMenuOpen = ( item: ITEM, ) => boolean | undefined; -export type NavbarPropOnItemSubMenuToggle = ( +export type NavbarPropOnSubMenuToggle = ( item: ITEM, open: boolean, params: { e?: React.MouseEvent }, @@ -162,7 +162,7 @@ export type NavbarProps< sortGroup?: NavbarPropSortGroup; getGroupAdditionalClassName?: NavbarPropGetGroupAdditionalClassName; getItemSubMenuOpen?: NavbarPropGetItemSubMenuOpen; - onItemSubMenuToggle?: NavbarPropOnItemSubMenuToggle; + onSubMenuToggle?: NavbarPropOnSubMenuToggle; }, HTMLDivElement > & @@ -232,7 +232,7 @@ export type NavbarItemProps = | undefined; level: number; getItemSubMenuOpen: NavbarPropGetItemSubMenuOpen | undefined; - onItemSubMenuToggle: NavbarPropOnItemSubMenuToggle | undefined; + onSubMenuToggle: NavbarPropOnSubMenuToggle | undefined; }, HTMLDivElement >; From d440d27bdfda18e8eed56cd5d27fe628cd07e035 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Wed, 5 Nov 2025 13:32:05 +0000 Subject: [PATCH 15/32] refactor(Navbar): rename onItemSubMenuToggle to onSubMenuToggle --- src/components/Navbar/Navbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index e598983..a59c891 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -38,7 +38,7 @@ const NavbarRender = (props: NavbarProps, ref: React.Ref) => { getItemSubMenu, getItemStatus, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, sortGroup, className, ...otherProps @@ -84,7 +84,7 @@ const NavbarRender = (props: NavbarProps, ref: React.Ref) => { getItemSubMenu={getItemSubMenu} getItemStatus={getItemStatus} getItemSubMenuOpen={getItemSubMenuOpen} - onItemSubMenuToggle={onItemSubMenuToggle} + onSubMenuToggle={onSubMenuToggle} form={form} /> From 9406a1fa868c986863737feaedf331c77b6c3529 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Wed, 5 Nov 2025 13:33:16 +0000 Subject: [PATCH 16/32] refactor(NavbarItem): implement controlled subMenu with onSubMenuToggle - Replaced onItemSubMenuToggle with onSubMenuToggle in handleToggle - Added useEffect for syncing controlled open state - Removed isControlled flag for simpler logic --- .../Navbar/NavbarItem/NavbarItem.tsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/Navbar/NavbarItem/NavbarItem.tsx b/src/components/Navbar/NavbarItem/NavbarItem.tsx index d3bd02e..18f0083 100644 --- a/src/components/Navbar/NavbarItem/NavbarItem.tsx +++ b/src/components/Navbar/NavbarItem/NavbarItem.tsx @@ -4,7 +4,7 @@ import { Badge } from '@consta/uikit/Badge'; import { ListItem } from '@consta/uikit/ListCanary'; import { useFlag } from '@consta/uikit/useFlag'; import { useForkRef } from '@consta/uikit/useForkRef'; -import React, { forwardRef } from 'react'; +import React, { forwardRef, useEffect } from 'react'; import { cn } from '##/utils/bem'; @@ -52,14 +52,20 @@ const NavbarItemRender = ( className, getItemStatus, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, level = 0, form, } = props; - const [open, setOpen] = useFlag(); - const isControlled = !!getItemSubMenuOpen && !!onItemSubMenuToggle; - const isOpen = isControlled ? getItemSubMenuOpen(item) ?? false : open; + const [open, setOpen] = useFlag(getItemSubMenuOpen?.(item) || false); + const controlledOpen = getItemSubMenuOpen?.(item); + + useEffect(() => { + if (controlledOpen !== undefined) { + setOpen.set(controlledOpen); + } + }, [controlledOpen]); + const subItems = getItemSubMenu?.(item); const rightSide = getItemRightSide?.(item); const active = getItemActive?.(item); @@ -67,12 +73,8 @@ const NavbarItemRender = ( const handleToggle = (e: React.MouseEvent) => { if (subItems?.length) { - const newOpen = !isOpen; - if (isControlled) { - onItemSubMenuToggle(item, newOpen, { e }); - } else { - setOpen.toggle(); - } + onSubMenuToggle?.(item, !open, { e }); + setOpen.set(!open); } }; @@ -94,12 +96,7 @@ const NavbarItemRender = ( ) : undefined, subItems?.length ? ( - { - handleToggle(e); - }} - /> + ) : undefined, ]} as={getItemAs?.(item)} @@ -116,7 +113,7 @@ const NavbarItemRender = ( ['--navbar-item-level-space' as string]: `var(--space-${mapLevelSpace[size]})`, }} /> - {isOpen && + {open && subItems?.map((subItem, index) => ( ))} From 0293424e339ff1dca84d7558b7a24b0384e88faf Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Wed, 5 Nov 2025 13:36:37 +0000 Subject: [PATCH 17/32] chore(Navbar): update tests for onSubMenuToggle rename and remove redundants --- .../NavbarItem/__tests__/NavbarItem.test.tsx | 179 +++++++----------- .../Navbar/__tests__/Navbar.test.tsx | 6 +- 2 files changed, 72 insertions(+), 113 deletions(-) diff --git a/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx b/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx index 7043aec..5c748c3 100644 --- a/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx +++ b/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx @@ -333,13 +333,13 @@ describe('Компонент NavbarItem', () => { it('должен вызывать onItemClick даже в контролируемом режиме', () => { const onItemClick = jest.fn(); const getItemSubMenuOpen = jest.fn().mockReturnValue(false); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: itemWithSubMenu, onItemClick, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); fireEvent.click(screen.getByText('Parent Item')); @@ -347,21 +347,19 @@ describe('Компонент NavbarItem', () => { expect(onItemClick).toHaveBeenCalledWith(itemWithSubMenu, { e: expect.any(Object), }); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemWithSubMenu, - true, - { e: expect.any(Object) }, - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + e: expect.any(Object), + }); }); it('должен отображать стрелку если есть subMenu в контролируемом режиме', () => { const getItemSubMenuOpen = jest.fn().mockReturnValue(false); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: itemWithSubMenu, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByRole('button')).toBeInTheDocument(); @@ -369,12 +367,12 @@ describe('Компонент NavbarItem', () => { it('должен использовать переданное значение открытия извне', () => { const getItemSubMenuOpen = jest.fn().mockReturnValue(true); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: itemWithSubMenu, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); @@ -383,81 +381,64 @@ describe('Компонент NavbarItem', () => { it('должен использовать переданное значение закрытия извне', () => { const getItemSubMenuOpen = jest.fn().mockReturnValue(false); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: itemWithSubMenu, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); expect(screen.queryByText('Sub Item 2')).not.toBeInTheDocument(); }); - it('должен вызывать onItemSubMenuToggle при клике, но не менять состояние самостоятельно', () => { + it('должен вызывать onSubMenuToggle при клике и поменять внутреннее состояние', () => { const getItemSubMenuOpen = jest.fn().mockReturnValue(false); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); - const { rerender } = renderComponent({ + renderComponent({ item: itemWithSubMenu, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); fireEvent.click(screen.getByText('Parent Item')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemWithSubMenu, - true, - { e: expect.any(Object) }, - ); - - expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); - - getItemSubMenuOpen.mockReturnValue(true); - rerender( - , - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + e: expect.any(Object), + }); - expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); + expect(screen.queryByText('Sub Item 1')).toBeInTheDocument(); }); it('должен работать полный цикл открытия/закрытия через контролируемое состояние', () => { let isOpen = false; const getItemSubMenuOpen = jest.fn(() => isOpen); - const onItemSubMenuToggle = jest.fn((item, newOpen) => { + const onSubMenuToggle = jest.fn((item, newOpen) => { isOpen = newOpen; }); const { rerender } = renderComponent({ item: itemWithSubMenu, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); fireEvent.click(screen.getByText('Parent Item')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemWithSubMenu, - true, - { e: expect.any(Object) }, - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + e: expect.any(Object), + }); rerender( , ); @@ -465,18 +446,16 @@ describe('Компонент NavbarItem', () => { expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); fireEvent.click(screen.getByText('Parent Item')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemWithSubMenu, - false, - { e: expect.any(Object) }, - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, false, { + e: expect.any(Object), + }); rerender( , ); @@ -487,31 +466,29 @@ describe('Компонент NavbarItem', () => { it('должен работать полный цикл открытия/закрытия через стрелку в контролируемом состоянии', () => { let isOpen = false; const getItemSubMenuOpen = jest.fn(() => isOpen); - const onItemSubMenuToggle = jest.fn((item, newOpen) => { + const onSubMenuToggle = jest.fn((item, newOpen) => { isOpen = newOpen; }); const { rerender } = renderComponent({ item: itemWithSubMenu, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemWithSubMenu, - true, - { e: expect.any(Object) }, - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + e: expect.any(Object), + }); rerender( , ); @@ -519,18 +496,16 @@ describe('Компонент NavbarItem', () => { expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemWithSubMenu, - false, - { e: expect.any(Object) }, - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, false, { + e: expect.any(Object), + }); rerender( , ); @@ -550,16 +525,16 @@ describe('Компонент NavbarItem', () => { expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); }); - it('должен вызывать и onItemClick и onItemSubMenuToggle при клике на элемент с subMenu', () => { + it('должен вызывать и onItemClick и onSubMenuToggle при клике на элемент с subMenu', () => { const onItemClick = jest.fn(); const getItemSubMenuOpen = jest.fn().mockReturnValue(false); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: itemWithSubMenu, onItemClick, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); fireEvent.click(screen.getByText('Parent Item')); @@ -567,23 +542,21 @@ describe('Компонент NavbarItem', () => { expect(onItemClick).toHaveBeenCalledWith(itemWithSubMenu, { e: expect.any(Object), }); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemWithSubMenu, - true, - { e: expect.any(Object) }, - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + e: expect.any(Object), + }); }); it('должен вызывать только onItemClick при клике на элемент без subMenu в контролируемом режиме', () => { const onItemClick = jest.fn(); const getItemSubMenuOpen = jest.fn().mockReturnValue(false); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: defaultItem, onItemClick, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); fireEvent.click(screen.getByText('Test Item')); @@ -591,7 +564,7 @@ describe('Компонент NavbarItem', () => { expect(onItemClick).toHaveBeenCalledWith(defaultItem, { e: expect.any(Object), }); - expect(onItemSubMenuToggle).not.toHaveBeenCalled(); + expect(onSubMenuToggle).not.toHaveBeenCalled(); }); }); @@ -619,12 +592,12 @@ describe('Компонент NavbarItem', () => { }; const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: deepNestedItems, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByText('Level 1')).toBeInTheDocument(); @@ -641,12 +614,12 @@ describe('Компонент NavbarItem', () => { }; const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: deepNestedItems, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByText('Level 1')).toBeInTheDocument(); @@ -663,14 +636,14 @@ describe('Компонент NavbarItem', () => { }; const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); - const onItemSubMenuToggle = jest.fn((item, newOpen) => { + const onSubMenuToggle = jest.fn((item, newOpen) => { openStates[item.label] = newOpen; }); const { rerender } = renderComponent({ item: deepNestedItems, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByText('Level 2')).toBeInTheDocument(); @@ -678,11 +651,9 @@ describe('Компонент NavbarItem', () => { expect(screen.getByText('Level 4')).toBeInTheDocument(); fireEvent.click(screen.getByText('Level 1')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - deepNestedItems, - false, - { e: expect.any(Object) }, - ); + expect(onSubMenuToggle).toHaveBeenCalledWith(deepNestedItems, false, { + e: expect.any(Object), + }); expect(openStates['Level 1']).toBe(false); rerender( @@ -690,7 +661,7 @@ describe('Компонент NavbarItem', () => { {...createNavbarItemProps({ item: deepNestedItems, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, })} />, ); @@ -711,12 +682,12 @@ describe('Компонент NavbarItem', () => { }; const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: deepNestedItems, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByText('Level 1')).toBeInTheDocument(); @@ -734,12 +705,12 @@ describe('Компонент NavbarItem', () => { }; const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: deepNestedItems, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByText('Level 1')).toBeInTheDocument(); @@ -861,7 +832,7 @@ describe('Компонент NavbarItem', () => { it('должен работать с кастомными геттерами для всех полей', () => { const onItemClick = jest.fn(); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: customItem, @@ -877,7 +848,7 @@ describe('Компонент NavbarItem', () => { getItemAttributes: (item: CustomNavbarItem) => item.htmlAttributes, onItemClick, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByText('Custom Item')).toBeInTheDocument(); @@ -896,34 +867,22 @@ describe('Компонент NavbarItem', () => { it('должен работать с кастомными геттерами для subMenu', () => { const getItemSubMenuOpen = jest.fn().mockReturnValue(false); - const onItemSubMenuToggle = jest.fn(); + const onSubMenuToggle = jest.fn(); renderComponent({ item: customItem, getItemLabel: (item: CustomNavbarItem) => item.name, getItemSubMenu: (item: CustomNavbarItem) => item.children, getItemSubMenuOpen, - onItemSubMenuToggle, + onSubMenuToggle, }); expect(screen.getByRole('button')).toBeInTheDocument(); fireEvent.click(screen.getByText('Custom Item')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith(customItem, true, { + expect(onSubMenuToggle).toHaveBeenCalledWith(customItem, true, { e: expect.any(Object), }); - }); - - it('должен работать с кастомными геттерами для вложенных элементов', () => { - renderComponent({ - item: customItem, - getItemLabel: (item: CustomNavbarItem) => item.name, - getItemSubMenu: (item: CustomNavbarItem) => item.children, - getItemSubMenuOpen: (item: CustomNavbarItem) => item.isActive, - onItemSubMenuToggle: jest.fn(), - }); - - fireEvent.click(screen.getByText('Custom Item')); expect(screen.getByText('Child Item')).toBeInTheDocument(); }); @@ -995,7 +954,7 @@ describe('Компонент NavbarItem', () => { getItemLabel: (item: any) => item.title, getItemSubMenu: (item: any) => item.items, getItemSubMenuOpen: (item: any) => item.expanded, - onItemSubMenuToggle: jest.fn((item, newOpen) => { + onSubMenuToggle: jest.fn((item, newOpen) => { item.expanded = newOpen; }), }); @@ -1010,7 +969,7 @@ describe('Компонент NavbarItem', () => { getItemLabel: (item: any) => item.title, getItemSubMenu: (item: any) => item.items, getItemSubMenuOpen: (item: any) => item.expanded, - onItemSubMenuToggle: jest.fn(), + onSubMenuToggle: jest.fn(), })} />, ); diff --git a/src/components/Navbar/__tests__/Navbar.test.tsx b/src/components/Navbar/__tests__/Navbar.test.tsx index 93ed555..89ca930 100644 --- a/src/components/Navbar/__tests__/Navbar.test.tsx +++ b/src/components/Navbar/__tests__/Navbar.test.tsx @@ -226,7 +226,7 @@ describe('Компонент Navbar', () => { render( , ); @@ -259,7 +259,7 @@ describe('Компонент Navbar', () => { , ); @@ -425,7 +425,7 @@ describe('Компонент Navbar', () => { form="round" size="s" onItemClick={onItemClick} - onItemSubMenuToggle={onItemSubMenuToggle} + onSubMenuToggle={onItemSubMenuToggle} />, ); From 62ded2b3b3fa855f35812dc7856f2b12c55b7541 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Wed, 5 Nov 2025 13:41:14 +0000 Subject: [PATCH 18/32] docs(Navbar): update API reference for subMenu toggle changes --- src/components/Navbar/__stand__/Navbar.dev.stand.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index b40191a..ac893ab 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -117,7 +117,7 @@ export const NavbarOnClickExample = () => ( ## Управление состоянием подменю -Для управления состоянием подменю (открыто/закрыто) используйте связку getItemSubMenuOpen и onItemSubMenuToggle. Это позволяет контролировать открытие подменю через внешний стейт, включая задание начального состояния. +Для управления состоянием подменю (открыто/закрыто) используйте связку getItemSubMenuOpen и onSubMenuToggle. Это позволяет контролировать открытие подменю через внешний стейт, включая задание начального состояния. ```tsx const [openStates, setOpenStates] = React.useState({ 'Пункт 1': true }); @@ -128,7 +128,7 @@ const menu = [ const getItemSubMenuOpen = (item) => openStates[item.label] || false; -const onItemSubMenuToggle = (item, open, { e }) => { +const onSubMenuToggle = (item, open, { e }) => { setOpenStates((prev) => ({ ...prev, [item.label]: open })); }; @@ -136,7 +136,7 @@ export const NavbarControlledExample = () => ( ); ``` @@ -225,7 +225,7 @@ type NavbarPropGetItemSubMenu = (item: ITEM) => ITEM[] | undefined; type NavbarPropGetItemSubMenuOpen = (item: ITEM) => boolean | undefined; -type NavbarPropOnItemSubMenuToggle = ( +type NavbarProponSubMenuToggle = ( item: ITEM, open: boolean, params: { e?: React.MouseEvent }, @@ -258,7 +258,7 @@ type NavbarPropOnItemSubMenuToggle = ( | `className?` | `string` | - | Дополнительный CSS-класс для пунктов меню | | `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | | `getItemSubMenuOpen?` | `NavbarPropGetItemSubMenuOpen` | - | Функция для определения открыто ли подменю элемента | -| `onItemSubMenuToggle?` | `NavbarPropOnItemSubMenuToggle` | - | Функция для обработки изменения состояния подменю элемента | +| `onSubMenuToggle?` | `NavbarProponSubMenuToggle` | - | Функция для обработки изменения состояния подменю элемента | ## NavbarRail From 66b893004f2516dd079b23b4ce50620812160e42 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Wed, 5 Nov 2025 13:50:27 +0000 Subject: [PATCH 19/32] docs(Navbar): update API reference for subMenu toggle types --- src/components/Navbar/__stand__/Navbar.dev.stand.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index ac893ab..75ef674 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -225,7 +225,7 @@ type NavbarPropGetItemSubMenu = (item: ITEM) => ITEM[] | undefined; type NavbarPropGetItemSubMenuOpen = (item: ITEM) => boolean | undefined; -type NavbarProponSubMenuToggle = ( +type NavbarProponOnSubMenuToggle = ( item: ITEM, open: boolean, params: { e?: React.MouseEvent }, @@ -258,7 +258,7 @@ type NavbarProponSubMenuToggle = ( | `className?` | `string` | - | Дополнительный CSS-класс для пунктов меню | | `ref?` | `React.Ref` | - | Ссылка на корневой DOM-элемент | | `getItemSubMenuOpen?` | `NavbarPropGetItemSubMenuOpen` | - | Функция для определения открыто ли подменю элемента | -| `onSubMenuToggle?` | `NavbarProponSubMenuToggle` | - | Функция для обработки изменения состояния подменю элемента | +| `onSubMenuToggle?` | `NavbarProponOnSubMenuToggle` | - | Функция для обработки изменения состояния подменю элемента | ## NavbarRail From be3fbf045f64aad0095999af5eab105707f2a206 Mon Sep 17 00:00:00 2001 From: gizeasy Date: Thu, 6 Nov 2025 12:00:58 +0300 Subject: [PATCH 20/32] chore(deps): update --- package.json | 4 +- yarn.lock | 299 +++++++++++++++++---------------------------------- 2 files changed, 101 insertions(+), 202 deletions(-) diff --git a/package.json b/package.json index 9a7600d..109f849 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "camelcase": "^6.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", "commitizen": "^4.2.5", - "compute-scroll-into-view": "^1.0.17", + "compute-scroll-into-view": "^3.1.1", "cross-env": "^7.0.3", "css-loader": "^6.5.1", "css-minimizer-webpack-plugin": "^3.4.1", @@ -170,7 +170,7 @@ "react-dev-utils": "^12.0.0", "react-dom": "^18.0.0", "react-dropzone": "^14.2.3", - "react-imask": "^7.2.1", + "react-imask": "^7.6.1", "react-refresh": "^0.11.0", "react-test-renderer": "^18.0.0", "react-textarea-autosize": "^8.5.3", diff --git a/yarn.lock b/yarn.lock index 59b5517..a128e2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1782,12 +1782,11 @@ integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== "@babel/runtime-corejs3@^7.24.4": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz#c766df350ec7a2caf3ed64e3659b100954589413" - integrity sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew== + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz#c25be39c7997ce2f130d70b9baecb8ed94df93fa" + integrity sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ== dependencies: - core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" + core-js-pure "^3.43.0" "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3": version "7.18.9" @@ -1796,16 +1795,13 @@ dependencies: regenerator-runtime "^0.13.4" -<<<<<<< Updated upstream -"@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" - integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== +"@babel/runtime@^7.20.13", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4" + integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA== dependencies: regenerator-runtime "^0.14.0" -======= ->>>>>>> Stashed changes "@babel/runtime@^7.20.7", "@babel/runtime@^7.9.2": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" @@ -1813,13 +1809,6 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.3.1": - version "7.22.11" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4" - integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/runtime@^7.8.4": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" @@ -1924,19 +1913,16 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -<<<<<<< Updated upstream "@bem-react/classname@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@bem-react/classname/-/classname-1.6.0.tgz#dbd1fc337b50fe726dae79c1c8184d7016aa6ccf" - integrity sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg== + version "1.7.0" + resolved "https://registry.yarnpkg.com/@bem-react/classname/-/classname-1.7.0.tgz#aca4e6f5faaedb5a51f6200201e1181c4e9f3d9c" + integrity sha512-WNZAJEVNHFpQ1eyR3SKxXUDHaXbTyMieFfC65tqEGvGxx9pMcaKf65v/IINdDBe6xIt6WgGu0EHgFQ5KH4lwZQ== "@bem-react/classnames@^1.3.10": version "1.3.10" resolved "https://registry.yarnpkg.com/@bem-react/classnames/-/classnames-1.3.10.tgz#858b3c25574dd3f2046cc1bf89300b9c2a3ff0f2" integrity sha512-tn+45Ii+S5FcYuO5FMs9YLSMUc355iUho7mwFeMMihi/ZZCQjvdR5AhVexnL9GS7pMtOeV0OsDOPDkW1sXVI3A== -======= ->>>>>>> Stashed changes "@bem/sdk.cell@^0.2.9": version "0.2.9" resolved "https://registry.yarnpkg.com/@bem/sdk.cell/-/sdk.cell-0.2.9.tgz#6d38431bbf734bd719bc137c11931c64a6047c71" @@ -2041,29 +2027,14 @@ chalk "^4.1.0" "@consta/header@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@consta/header/-/header-3.0.1.tgz#6c14e61aa3d162ead7abb2e54a91cf1ddc346f12" - integrity sha512-TfMkt/2OqmbZGNpJrEcvwh6ZSious5R6p1qi1N7Dk9iOSnfxLAfjGHbI8dzphReJ3/CsKah0ZFo6AxEvOyR9yQ== - -<<<<<<< Updated upstream -"@consta/icons@^1.0.1", "@consta/icons@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@consta/icons/-/icons-1.1.0.tgz#f21496bc833ad062f03ddde62b93bdf7631c3c57" - integrity sha512-vsv41yw3atSzXfrmu9rJSNvcxjChrFau0IMAP/4nxBPzF7AIFx9Jf94ZtOOmK+st0NN/HXqKkaFMey6rYXTUCQ== + version "3.0.2" + resolved "https://registry.yarnpkg.com/@consta/header/-/header-3.0.2.tgz#6b251bf03574171748f5f7294d3429110e7e3fff" + integrity sha512-0HkAnI0D5zQHvLgdBqA8K5jk0Cpnl1Ffn6PrIpbx4MSrTPRzmafoqgyfvzmL8Issr5wEXGSpaYDB+/x/psNuaw== -"@consta/stand@^0.0.150": - version "0.0.150" - resolved "https://registry.yarnpkg.com/@consta/stand/-/stand-0.0.150.tgz#426c915b63f5045c8a8055b6db5ed98aca03e5e7" - integrity sha512-OGYc/HTpz5TIm6flW+Wk551RbXZT+Xapkzxu3rXOMWSYLTO9hVUaEO+7wOAy7p+2ZJYXix5ZZSl2WIS0fKgGcg== - dependencies: - "@consta/header" "^3.0.1" - "@consta/icons" "^1.1.0" - "@consta/uikit" "^5.18.0" -======= "@consta/icons@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@consta/icons/-/icons-1.1.1.tgz#a6b9535612d4b78228ebc441123c11f42b2ee000" - integrity sha512-3meYl46x0figWwTwakB19q+2BYuXWeBAyxQgBdxYdclocKkuMztF/ffknIQjPdfjHeA6rBwIbbxh7M3BfTXH5A== + version "1.4.0" + resolved "https://registry.yarnpkg.com/@consta/icons/-/icons-1.4.0.tgz#f76a0d0d0e8caa75d779f14f709969d5c51807ce" + integrity sha512-V9KwdJHmL05vFqeKhJRjIbhaoa0EMeyvrISSpTWANwxOKUkffwQjiL68fW5jipVfZdJSXlvo3C0oZ6J3eZehTg== "@consta/stand@^0.0.155": version "0.0.155" @@ -2073,7 +2044,6 @@ "@consta/header" "^3.0.1" "@consta/icons" "^1.1.1" "@consta/uikit" "^5.22.0" ->>>>>>> Stashed changes "@mdx-js/loader" "^2.3.0" "@mdx-js/react" "^2.3.0" "@oclif/command" "^1.8.22" @@ -2140,17 +2110,10 @@ webpack-manifest-plugin "^5.0.0" workbox-webpack-plugin "^6.4.1" -<<<<<<< Updated upstream -"@consta/uikit@^5.0.0", "@consta/uikit@^5.18.0": - version "5.18.0" - resolved "https://registry.yarnpkg.com/@consta/uikit/-/uikit-5.18.0.tgz#0245164f544664558d4147aa48d84ffc9519f756" - integrity sha512-320Znjew504vdEmijflybYxY2R+RmD767leschzW3GDna/4T9aLUHT+9Zt4GM+doT99wh+nx9IOYKeNHbsagFA== -======= "@consta/uikit@^5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@consta/uikit/-/uikit-5.22.0.tgz#371c310788e92e5c2fcaf3571ca14c4292d77810" - integrity sha512-Fu1K7ZOLFW8ZSR+JWM31nBKcebkGlksvqyx1wlqlP0M/C34QOKDvzfMmNNe1TGH2tCSbnJZhZatOSo60oXhNVA== ->>>>>>> Stashed changes + version "5.26.0" + resolved "https://registry.yarnpkg.com/@consta/uikit/-/uikit-5.26.0.tgz#db188813404cd58486ceae7b7a3a44172cc390fb" + integrity sha512-rbkrl4PPVsg+Ly8UtC4B8y1VD0cbOHFGTpNrDg6qsUyuSAqT1e80XGgzefACmEmlh4NDt9kexWiUIpdlckvZ2Q== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -2888,39 +2851,29 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@reatom/async@^3.16.8": - version "3.16.8" - resolved "https://registry.yarnpkg.com/@reatom/async/-/async-3.16.8.tgz#436171e38e3c9c08a2f9a33b333397c799242d5c" - integrity sha512-9x+mPFZMbAtEpLsnF4s0dVQE2e1K0/lE8DA8F9OeDwFzdZqQhQLpirbfGEh+NYHOVdYk9Yj6lhm6O7TBLGHnKg== +"@reatom/async@^3.16.10": + version "3.16.10" + resolved "https://registry.yarnpkg.com/@reatom/async/-/async-3.16.10.tgz#fdc8e2962db7d08c49c7862e879a39049421bc18" + integrity sha512-HxZwS7EydTcBKoKd3Z2VIi0O8YjJrkYWLSH0wxEPUhZcuWILjyuYSFBSAq4wFFkvQlbh9L7g6T4hr7lhQDhkbA== dependencies: - "@reatom/core" "^3.5.0" - "@reatom/effects" "^3.10.0" - "@reatom/hooks" "^3.2.0" - "@reatom/primitives" "^3.5.0" - "@reatom/utils" "^3.11.0" - -<<<<<<< Updated upstream -"@reatom/core@^3.1.1", "@reatom/core@^3.10.1", "@reatom/core@^3.2.0", "@reatom/core@^3.3.0", "@reatom/core@^3.4.0", "@reatom/core@^3.5.0", "@reatom/core@^3.8.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@reatom/core/-/core-3.10.1.tgz#b91f562cce25610539618c7fc75b5fb9c44d338a" - integrity sha512-A5vx+akCGkc+YCYhqPaAnR46uvqe70pQ2JB82JCLgOrj+YmnStIGkiaiWG43wn30qUjatXjejJmGkqQbjtri+A== + "@reatom/core" "^3.10.3" + "@reatom/effects" "^3.11.3" + "@reatom/hooks" "^3.6.1" + "@reatom/persist" "^3.4.3" + "@reatom/primitives" "^3.11.0" + "@reatom/utils" "^3.11.3" -"@reatom/effects@^3.10.0", "@reatom/effects@^3.11.3", "@reatom/effects@^3.2.0", "@reatom/effects@^3.7.0", "@reatom/effects@^3.7.3": - version "3.11.3" - resolved "https://registry.yarnpkg.com/@reatom/effects/-/effects-3.11.3.tgz#e6ff0901d7cf7c0fad01a52019a35d8768cf0566" - integrity sha512-0qxr7m6e+GrOvt0pESONl4aRZxGjsU1HWXIsDR2Ghw0mNGjuStnEDUZnO+MVbKOArMIAvZ8ZoMrQWqXEBfOrVg== -======= "@reatom/core@^3.1.1", "@reatom/core@^3.2.0", "@reatom/core@^3.3.0", "@reatom/core@^3.4.0", "@reatom/core@^3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@reatom/core/-/core-3.5.0.tgz#d5b6bf9611a18306336ca298e15a2cdcdc60f5c1" integrity sha512-zCprMJQY1rsQOc3MU91d0ij0hgR7MPyYyZKqg29vSlrPtulYKx8FFClP78zm/QYt3XOizq8wIyGfeRQiU1hKKw== -"@reatom/core@^3.10.1", "@reatom/core@^3.8.1": - version "3.10.1" - resolved "https://registry.yarnpkg.com/@reatom/core/-/core-3.10.1.tgz#b91f562cce25610539618c7fc75b5fb9c44d338a" - integrity sha512-A5vx+akCGkc+YCYhqPaAnR46uvqe70pQ2JB82JCLgOrj+YmnStIGkiaiWG43wn30qUjatXjejJmGkqQbjtri+A== +"@reatom/core@^3.10.3", "@reatom/core@^3.8.1": + version "3.10.3" + resolved "https://registry.yarnpkg.com/@reatom/core/-/core-3.10.3.tgz#ff9d234fe25fb847e488de12a91cfec1b04ba510" + integrity sha512-uctBfmL/ZNvHWRX92Y+VQZON37fXNja4FM1fzevX7RYOWy6R7evx42VM6K4kuOaIci7bVWPhPZzHWeAu0J2lHQ== -"@reatom/effects@^3.10.0", "@reatom/effects@^3.11.3", "@reatom/effects@^3.7.0", "@reatom/effects@^3.7.3": +"@reatom/effects@^3.11.3", "@reatom/effects@^3.7.0", "@reatom/effects@^3.7.3": version "3.11.3" resolved "https://registry.yarnpkg.com/@reatom/effects/-/effects-3.11.3.tgz#e6ff0901d7cf7c0fad01a52019a35d8768cf0566" integrity sha512-0qxr7m6e+GrOvt0pESONl4aRZxGjsU1HWXIsDR2Ghw0mNGjuStnEDUZnO+MVbKOArMIAvZ8ZoMrQWqXEBfOrVg== @@ -2932,59 +2885,32 @@ version "3.5.0" resolved "https://registry.yarnpkg.com/@reatom/effects/-/effects-3.5.0.tgz#57a27e0090eba9132f1281db59dd6230af24136c" integrity sha512-rR9xhaGUCEEhYP8EvLq6qYipmKlQQGSF5N0RmIHVzYorUTIK0a2BCMpq54PtHFKBMiEDhSu4UAsdd23yBIJmog== ->>>>>>> Stashed changes dependencies: "@reatom/core" "^3.2.0" "@reatom/utils" "^3.5.0" -<<<<<<< Updated upstream "@reatom/framework@^3.4.6", "@reatom/framework@^3.4.63": - version "3.4.63" - resolved "https://registry.yarnpkg.com/@reatom/framework/-/framework-3.4.63.tgz#c8273b1fd72fcbc1fbadf8c5036538cd384e2d59" - integrity sha512-LWPlYfTzJ0jCHIeN9T3/5JJ/BMKf7IYFBJakb1W4QviIIoIM07cT/GL6o2TXswKgf49eJQ0Jyw+Pts61wZf1Jw== -======= -"@reatom/framework@^3.4.63": - version "3.4.64" - resolved "https://registry.yarnpkg.com/@reatom/framework/-/framework-3.4.64.tgz#33028f32366fe669c1ee7bb23fdea6ce5bf27896" - integrity sha512-ev0CLSBk9eAr7+sdw5NJg2Dax8RXFitGoDbBag0suYYIb6x91sHCdsDFV4PTWGnXEvwX3+KYmREsVZuUO6lFGA== ->>>>>>> Stashed changes - dependencies: - "@reatom/async" "^3.16.8" - "@reatom/core" "^3.10.1" + version "3.4.68" + resolved "https://registry.yarnpkg.com/@reatom/framework/-/framework-3.4.68.tgz#ecbf80f90808eafdd0d3440ecddd1a83834db381" + integrity sha512-8/ACG5O0z2c0GSFCFjogcHFXzYJ9+fGuUD0dzAV9gjHzDiziYujvFjyJj9g7wmAynI7UYR8cLClJ8WjVAOlH+g== + dependencies: + "@reatom/async" "^3.16.10" + "@reatom/core" "^3.10.3" "@reatom/effects" "^3.11.3" "@reatom/hooks" "^3.6.1" -<<<<<<< Updated upstream - "@reatom/lens" "^3.11.8" - "@reatom/logger" "^3.8.4" - "@reatom/primitives" "^3.10.0" - "@reatom/utils" "^3.11.3" - -"@reatom/hooks@^3.2.0", "@reatom/hooks@^3.3.1", "@reatom/hooks@^3.4.0", "@reatom/hooks@^3.6.1": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@reatom/hooks/-/hooks-3.6.1.tgz#bac1d50ba7cebdbb50af1947c65e919387feb8cc" - integrity sha512-1q8qXAOkQlDKc/Y94alPHWqMnXvJhCG4Rr9hQxPMPG1Qf3WpeKm7Zdxs4v3DC2Kcw6oG6djVk3i5duIjPygGWA== -======= "@reatom/lens" "^3.12.0" "@reatom/logger" "^3.8.4" "@reatom/primitives" "^3.11.0" "@reatom/utils" "^3.11.3" -"@reatom/hooks@^3.2.0", "@reatom/hooks@^3.3.1", "@reatom/hooks@^3.4.0": +"@reatom/hooks@^3.3.1", "@reatom/hooks@^3.4.0": version "3.4.2" resolved "https://registry.yarnpkg.com/@reatom/hooks/-/hooks-3.4.2.tgz#06b82cbab2a9e0c201d650fc5122fa997cd06839" integrity sha512-IWr2mNlCc9QtB1VGoiMy1fT9mTDyh/1CW12tJCD4FsrWmtHmXwUs1IuGJYCLFAhaJ7VIeBRnK53bHrjokHra2Q== ->>>>>>> Stashed changes dependencies: "@reatom/core" "^3.2.0" - "@reatom/effects" "^3.7.0" "@reatom/utils" "^3.3.0" -<<<<<<< Updated upstream -"@reatom/lens@^3.1.0", "@reatom/lens@^3.11.8", "@reatom/lens@^3.4.0": - version "3.11.8" - resolved "https://registry.yarnpkg.com/@reatom/lens/-/lens-3.11.8.tgz#14847bbb1795ff62854d0859a1e376f5139e96bc" - integrity sha512-MiHrOI7to7065+UDbG+Qbs4n1PCub/O9OcOpOIYR9SB9+pZrl05lvNQXT4QLqfEXO5coYUZ0JbK2SkR1kvO5Yg== -======= "@reatom/hooks@^3.6.1": version "3.6.1" resolved "https://registry.yarnpkg.com/@reatom/hooks/-/hooks-3.6.1.tgz#bac1d50ba7cebdbb50af1947c65e919387feb8cc" @@ -2998,24 +2924,12 @@ version "3.6.2" resolved "https://registry.yarnpkg.com/@reatom/lens/-/lens-3.6.2.tgz#bfda0f4567baa0bc360b5fea77134a0b4e92a9a5" integrity sha512-nyMZ98diwQ19RHDcXURY8k7F+oSSLFH+SAwoQn/MWjn7qkCleUc4fRP3NJHsKh7C4fCi1DtOFEqnerRVJf7rjQ== ->>>>>>> Stashed changes dependencies: "@reatom/core" "^3.4.0" "@reatom/effects" "^3.2.0" "@reatom/hooks" "^3.3.1" - "@reatom/primitives" "^3.6.0" "@reatom/utils" "^3.1.0" -<<<<<<< Updated upstream -"@reatom/logger@^3.8.4": - version "3.8.4" - resolved "https://registry.yarnpkg.com/@reatom/logger/-/logger-3.8.4.tgz#88880b7328de22cc12bd6c15f63606f5c667e346" - integrity sha512-MOz8Td1eZV+kU4QpkZXAdO9qFtGjqpm40crIlMNweDtOH7GgUmV2oKgOXRORQzYbeGHVMlQHG4J5iPeEQdM7KA== - dependencies: - "@reatom/core" "^3.8.1" - "@reatom/utils" "^3.9.0" - -======= "@reatom/lens@^3.12.0": version "3.12.0" resolved "https://registry.yarnpkg.com/@reatom/lens/-/lens-3.12.0.tgz#f297ce7bab2146a799fc4f44881e3eb7039b2dc1" @@ -3035,7 +2949,6 @@ "@reatom/core" "^3.8.1" "@reatom/utils" "^3.9.0" ->>>>>>> Stashed changes "@reatom/npm-react@^3.10.6": version "3.10.6" resolved "https://registry.yarnpkg.com/@reatom/npm-react/-/npm-react-3.10.6.tgz#f9f7b1b543fe8ba4a5ab122aeb91517ba2902ebe" @@ -3057,36 +2970,39 @@ idb-keyval "^6.2.1" "@reatom/persist@^3.1.0": - version "3.4.1" - resolved "https://registry.yarnpkg.com/@reatom/persist/-/persist-3.4.1.tgz#2cef471b7a3bf907bf7a9bb29741c6f702f9934e" - integrity sha512-LM3JriTJNSH1EluVcvW9ik7DK5oa0NeIgkz8rIGvVk/c9ZIqp0Olthc/WEB5qNNDdzZMrebGRFiHRM2iO5/T6A== + version "3.3.0" + resolved "https://registry.yarnpkg.com/@reatom/persist/-/persist-3.3.0.tgz#1adafc5e04fb75cf586d89963bfc24e5fa3263a9" + integrity sha512-RM9PFxjwLje1FsMfBsborA9SyedsJIJVHqfuyoszLRv3J9SmTi4S2SJBd2EfZBbGF1hy0vwPncx1FYIHbgmU9g== dependencies: "@reatom/core" "^3.3.0" "@reatom/hooks" "^3.4.0" "@reatom/lens" "^3.4.0" "@reatom/utils" "^3.4.0" -<<<<<<< Updated upstream -"@reatom/primitives@^3.10.0", "@reatom/primitives@^3.5.0", "@reatom/primitives@^3.6.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@reatom/primitives/-/primitives-3.10.0.tgz#69429a43650e6aa10e912a593e30999ac8c813c9" - integrity sha512-sGsWcE1UE47BeT5YpV1thVrLJFP7G//k/3yM7nol8HQFfWQAiDZvtfC5VyFdm+IlHorch6SzY5Xva0D4oqqGrQ== -======= -"@reatom/primitives@^3.11.0", "@reatom/primitives@^3.5.0", "@reatom/primitives@^3.6.0": +"@reatom/persist@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@reatom/persist/-/persist-3.4.3.tgz#911018e195c18276bc590cee2f68c1d749f83e8d" + integrity sha512-NsJNZEATjaXKGjxlPQ95KD3LmEVD4SY01HSJmDujACOYECFUMrVBD3p+SPV2+2PJEfVXAVapFvqxwPCq3RlajA== + dependencies: + "@reatom/core" "^3.10.3" + "@reatom/hooks" "^3.6.1" + "@reatom/lens" "^3.12.0" + "@reatom/utils" "^3.11.3" + +"@reatom/primitives@^3.11.0", "@reatom/primitives@^3.6.0": version "3.11.0" resolved "https://registry.yarnpkg.com/@reatom/primitives/-/primitives-3.11.0.tgz#fb901f85b640e321544d40cc858258b8cd342d42" integrity sha512-b+jtK7qpQxSP83mYQXpPRMlFyg+C9WPv4sZDWSmm00mwmwetW0KbltftkWiwQjQM1TLwpDLZ6R7DX7fcTraIgg== ->>>>>>> Stashed changes dependencies: "@reatom/core" "^3.1.1" "@reatom/utils" "^3.1.1" -"@reatom/utils@^3.1.0", "@reatom/utils@^3.1.1", "@reatom/utils@^3.11.0", "@reatom/utils@^3.11.3", "@reatom/utils@^3.3.0", "@reatom/utils@^3.4.0", "@reatom/utils@^3.5.0", "@reatom/utils@^3.9.0": - version "3.11.3" - resolved "https://registry.yarnpkg.com/@reatom/utils/-/utils-3.11.3.tgz#89fe7ddcb049c6c5f506410d44f66cf2377df8ec" - integrity sha512-H2FQf9xra7Twf0PxS6L0DtuRRC79NfHRB3V/YhnhPHyUE/UFscXrin/I2eGj3FEKcgOfC2BWnzCOrkXjKWgECQ== +"@reatom/utils@^3.1.0", "@reatom/utils@^3.1.1", "@reatom/utils@^3.3.0", "@reatom/utils@^3.4.0", "@reatom/utils@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@reatom/utils/-/utils-3.5.0.tgz#f7db4fc624cb240300edc1f32a92ba631338a4ab" + integrity sha512-E68mAlau+CBjxKL5oFUbKlEIP5eireHFp7Z62HRFrfmnekssgXHxh0wfUJQUMVdMl7KSGytHJBBDReq9j4Hr/w== -"@reatom/utils@^3.11.0", "@reatom/utils@^3.11.3", "@reatom/utils@^3.9.0": +"@reatom/utils@^3.11.3", "@reatom/utils@^3.9.0": version "3.11.3" resolved "https://registry.yarnpkg.com/@reatom/utils/-/utils-3.11.3.tgz#89fe7ddcb049c6c5f506410d44f66cf2377df8ec" integrity sha512-H2FQf9xra7Twf0PxS6L0DtuRRC79NfHRB3V/YhnhPHyUE/UFscXrin/I2eGj3FEKcgOfC2BWnzCOrkXjKWgECQ== @@ -3736,9 +3652,9 @@ csstype "^3.0.2" "@types/react@^18.3.12": - version "18.3.23" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.23.tgz#86ae6f6b95a48c418fecdaccc8069e0fbb63696a" - integrity sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w== + version "18.3.26" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.26.tgz#4c5970878d30db3d2a0bca1e4eb5f258e391bbeb" + integrity sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA== dependencies: "@types/prop-types" "*" csstype "^3.0.2" @@ -4622,14 +4538,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -<<<<<<< Updated upstream attr-accept@^2.2.4: version "2.2.5" resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e" integrity sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ== -======= ->>>>>>> Stashed changes author-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/author-regex/-/author-regex-1.0.0.tgz#d08885be6b9bbf9439fe087c76287245f0a81450" @@ -6159,6 +6072,11 @@ compute-scroll-into-view@^1.0.17: resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43" integrity sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg== +compute-scroll-into-view@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa" + integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -6299,10 +6217,10 @@ core-js-pure@^3.23.3: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.32.1.tgz#5775b88f9062885f67b6d7edce59984e89d276f3" integrity sha512-f52QZwkFVDPf7UEQZGHKx6NYxsxmVGJe5DIvbzOdRMJlmT6yv0KDjR8rmy3ngr/t5wU54c7Sp/qIJH0ppbhVpQ== -core-js-pure@^3.30.2: - version "3.41.0" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.41.0.tgz#349fecad168d60807a31e83c99d73d786fe80811" - integrity sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q== +core-js-pure@^3.43.0: + version "3.46.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.46.0.tgz#9bb80248584c6334bb54cd381b0f41c619ef1b43" + integrity sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw== core-js-pure@^3.8.1: version "3.24.0" @@ -6700,7 +6618,6 @@ data-urls@^3.0.1: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -<<<<<<< Updated upstream date-fns@^2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" @@ -6708,8 +6625,6 @@ date-fns@^2.30.0: dependencies: "@babel/runtime" "^7.21.0" -======= ->>>>>>> Stashed changes debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -7064,6 +6979,14 @@ dom-converter@^0.2.0: dependencies: utila "~0.4" +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -8252,7 +8175,6 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" -<<<<<<< Updated upstream file-selector@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-2.1.2.tgz#fe7c7ee9e550952dfbc863d73b14dc740d7de8b4" @@ -8260,8 +8182,6 @@ file-selector@^2.1.0: dependencies: tslib "^2.7.0" -======= ->>>>>>> Stashed changes filelist@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" @@ -9429,15 +9349,9 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== idb-keyval@^6.2.1: -<<<<<<< Updated upstream - version "6.2.1" - resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" - integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== -======= version "6.2.2" resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a" integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg== ->>>>>>> Stashed changes idb@^7.0.1: version "7.0.2" @@ -9483,19 +9397,17 @@ ignore@^5.0.6, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -<<<<<<< Updated upstream +ignore@^5.1.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + imask@^7.6.1: version "7.6.1" resolved "https://registry.yarnpkg.com/imask/-/imask-7.6.1.tgz#04fa4693bf47a4a71bbf7325408e0d058a74dcad" integrity sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg== dependencies: "@babel/runtime-corejs3" "^7.24.4" -======= -ignore@^5.1.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== ->>>>>>> Stashed changes immer@^9.0.7: version "9.0.15" @@ -14602,11 +14514,7 @@ promzard@^0.3.0: dependencies: read "1" -<<<<<<< Updated upstream prop-types@^15.6.2, prop-types@^15.8.1: -======= -prop-types@^15.8.1: ->>>>>>> Stashed changes version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -14843,7 +14751,6 @@ react-dom@^18.0.0: loose-envify "^1.1.0" scheduler "^0.23.0" -<<<<<<< Updated upstream react-dropzone@^14.2.3: version "14.3.8" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.3.8.tgz#a7eab118f8a452fe3f8b162d64454e81ba830582" @@ -14853,8 +14760,6 @@ react-dropzone@^14.2.3: file-selector "^2.1.0" prop-types "^15.8.1" -======= ->>>>>>> Stashed changes react-error-boundary@^3.1.0: version "3.1.4" resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" @@ -14874,7 +14779,7 @@ react-highlight@^0.14.0: dependencies: highlight.js "^10.5.0" -react-imask@^7.2.1: +react-imask@^7.6.1: version "7.6.1" resolved "https://registry.yarnpkg.com/react-imask/-/react-imask-7.6.1.tgz#40dbb03f0c9b2652a16450ff29a53581b5ae773d" integrity sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA== @@ -14937,7 +14842,6 @@ react-test-renderer@^18.0.0: react-shallow-renderer "^16.15.0" scheduler "^0.23.0" -<<<<<<< Updated upstream react-textarea-autosize@^8.5.3: version "8.5.9" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz#ab8627b09aa04d8a2f45d5b5cd94c84d1d4a8893" @@ -14957,8 +14861,6 @@ react-transition-group@^4.4.5: loose-envify "^1.4.0" prop-types "^15.6.2" -======= ->>>>>>> Stashed changes react@^18.0.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -17486,9 +17388,9 @@ typescript@^4.6.4: integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== typescript@^5.8.3: - version "5.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" - integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== uid-number@0.0.6: version "0.0.6" @@ -17818,26 +17720,23 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" -<<<<<<< Updated upstream use-composed-ref@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.4.0.tgz#09e023bf798d005286ad85cd20674bdf5770653b" - integrity sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w== + version "1.3.0" + resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" + integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== use-isomorphic-layout-effect@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz#afb292eb284c39219e8cb8d3d62d71999361a21d" - integrity sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w== + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" + integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== use-latest@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.3.0.tgz#549b9b0d4c1761862072f0899c6f096eb379137a" - integrity sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2" + integrity sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw== dependencies: use-isomorphic-layout-effect "^1.1.1" -======= ->>>>>>> Stashed changes use-subscription@^1.5.1: version "1.8.0" resolved "https://registry.yarnpkg.com/use-subscription/-/use-subscription-1.8.0.tgz#f118938c29d263c2bce12fc5585d3fe694d4dbce" From 4b791ae58473938063812d1a0e86c90cbf6e6f7a Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Thu, 6 Nov 2025 11:58:33 +0000 Subject: [PATCH 21/32] fix(Navbar): update onSubMenuToggle callback signature --- src/components/Navbar/types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Navbar/types.ts b/src/components/Navbar/types.ts index 2e36891..7449864 100644 --- a/src/components/Navbar/types.ts +++ b/src/components/Navbar/types.ts @@ -102,8 +102,10 @@ export type NavbarPropGetItemSubMenuOpen = ( export type NavbarPropOnSubMenuToggle = ( item: ITEM, - open: boolean, - params: { e?: React.MouseEvent }, + params: { + open: boolean; + e?: React.MouseEvent; + }, ) => void; // GROUPS From 6542f7e665781ea62badf55c7bb1d6194e018f1b Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Thu, 6 Nov 2025 12:00:48 +0000 Subject: [PATCH 22/32] fix(NavbarItem): update onSubMenuToggle usage and reorder effects --- src/components/Navbar/NavbarItem/NavbarItem.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/Navbar/NavbarItem/NavbarItem.tsx b/src/components/Navbar/NavbarItem/NavbarItem.tsx index 18f0083..f272c2c 100644 --- a/src/components/Navbar/NavbarItem/NavbarItem.tsx +++ b/src/components/Navbar/NavbarItem/NavbarItem.tsx @@ -60,12 +60,6 @@ const NavbarItemRender = ( const [open, setOpen] = useFlag(getItemSubMenuOpen?.(item) || false); const controlledOpen = getItemSubMenuOpen?.(item); - useEffect(() => { - if (controlledOpen !== undefined) { - setOpen.set(controlledOpen); - } - }, [controlledOpen]); - const subItems = getItemSubMenu?.(item); const rightSide = getItemRightSide?.(item); const active = getItemActive?.(item); @@ -73,11 +67,17 @@ const NavbarItemRender = ( const handleToggle = (e: React.MouseEvent) => { if (subItems?.length) { - onSubMenuToggle?.(item, !open, { e }); + onSubMenuToggle?.(item, { open: !open, e }); setOpen.set(!open); } }; + useEffect(() => { + if (controlledOpen !== undefined) { + setOpen.set(controlledOpen); + } + }, [controlledOpen]); + return ( <> Date: Thu, 6 Nov 2025 12:02:16 +0000 Subject: [PATCH 23/32] chore(Navbar): update tests for new onSubMenuToggle signature --- .../NavbarItem/__tests__/NavbarItem.test.tsx | 43 +++++++++++-------- .../Navbar/__tests__/Navbar.test.tsx | 9 ++-- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx b/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx index 5c748c3..d493d2e 100644 --- a/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx +++ b/src/components/Navbar/NavbarItem/__tests__/NavbarItem.test.tsx @@ -347,7 +347,8 @@ describe('Компонент NavbarItem', () => { expect(onItemClick).toHaveBeenCalledWith(itemWithSubMenu, { e: expect.any(Object), }); - expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, { + open: true, e: expect.any(Object), }); }); @@ -406,7 +407,8 @@ describe('Компонент NavbarItem', () => { expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); fireEvent.click(screen.getByText('Parent Item')); - expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, { + open: true, e: expect.any(Object), }); @@ -416,8 +418,8 @@ describe('Компонент NavbarItem', () => { it('должен работать полный цикл открытия/закрытия через контролируемое состояние', () => { let isOpen = false; const getItemSubMenuOpen = jest.fn(() => isOpen); - const onSubMenuToggle = jest.fn((item, newOpen) => { - isOpen = newOpen; + const onSubMenuToggle = jest.fn((item, params) => { + isOpen = params.open; }); const { rerender } = renderComponent({ @@ -429,7 +431,8 @@ describe('Компонент NavbarItem', () => { expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); fireEvent.click(screen.getByText('Parent Item')); - expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, { + open: true, e: expect.any(Object), }); @@ -446,7 +449,8 @@ describe('Компонент NavbarItem', () => { expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); fireEvent.click(screen.getByText('Parent Item')); - expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, false, { + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, { + open: false, e: expect.any(Object), }); @@ -466,8 +470,8 @@ describe('Компонент NavbarItem', () => { it('должен работать полный цикл открытия/закрытия через стрелку в контролируемом состоянии', () => { let isOpen = false; const getItemSubMenuOpen = jest.fn(() => isOpen); - const onSubMenuToggle = jest.fn((item, newOpen) => { - isOpen = newOpen; + const onSubMenuToggle = jest.fn((item, params) => { + isOpen = params.open; }); const { rerender } = renderComponent({ @@ -479,7 +483,8 @@ describe('Компонент NavbarItem', () => { expect(screen.queryByText('Sub Item 1')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button')); - expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, { + open: true, e: expect.any(Object), }); @@ -496,7 +501,8 @@ describe('Компонент NavbarItem', () => { expect(screen.getByText('Sub Item 1')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button')); - expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, false, { + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, { + open: false, e: expect.any(Object), }); @@ -542,7 +548,8 @@ describe('Компонент NavbarItem', () => { expect(onItemClick).toHaveBeenCalledWith(itemWithSubMenu, { e: expect.any(Object), }); - expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, true, { + expect(onSubMenuToggle).toHaveBeenCalledWith(itemWithSubMenu, { + open: true, e: expect.any(Object), }); }); @@ -636,8 +643,8 @@ describe('Компонент NavbarItem', () => { }; const getItemSubMenuOpen = jest.fn((item) => openStates[item.label]); - const onSubMenuToggle = jest.fn((item, newOpen) => { - openStates[item.label] = newOpen; + const onSubMenuToggle = jest.fn((item, params) => { + openStates[item.label] = params.open; }); const { rerender } = renderComponent({ @@ -651,7 +658,8 @@ describe('Компонент NavbarItem', () => { expect(screen.getByText('Level 4')).toBeInTheDocument(); fireEvent.click(screen.getByText('Level 1')); - expect(onSubMenuToggle).toHaveBeenCalledWith(deepNestedItems, false, { + expect(onSubMenuToggle).toHaveBeenCalledWith(deepNestedItems, { + open: false, e: expect.any(Object), }); @@ -880,7 +888,8 @@ describe('Компонент NavbarItem', () => { expect(screen.getByRole('button')).toBeInTheDocument(); fireEvent.click(screen.getByText('Custom Item')); - expect(onSubMenuToggle).toHaveBeenCalledWith(customItem, true, { + expect(onSubMenuToggle).toHaveBeenCalledWith(customItem, { + open: true, e: expect.any(Object), }); @@ -954,8 +963,8 @@ describe('Компонент NavbarItem', () => { getItemLabel: (item: any) => item.title, getItemSubMenu: (item: any) => item.items, getItemSubMenuOpen: (item: any) => item.expanded, - onSubMenuToggle: jest.fn((item, newOpen) => { - item.expanded = newOpen; + onSubMenuToggle: jest.fn((item, params) => { + item.expanded = params.open; }), }); diff --git a/src/components/Navbar/__tests__/Navbar.test.tsx b/src/components/Navbar/__tests__/Navbar.test.tsx index 89ca930..cd488a4 100644 --- a/src/components/Navbar/__tests__/Navbar.test.tsx +++ b/src/components/Navbar/__tests__/Navbar.test.tsx @@ -233,11 +233,10 @@ describe('Компонент Navbar', () => { fireEvent.click(screen.getByText('Parent Item')); - expect(onItemSubMenuToggle).toHaveBeenCalledWith( - itemsWithSubMenu[0], - true, - { e: expect.any(Object) }, - ); + expect(onItemSubMenuToggle).toHaveBeenCalledWith(itemsWithSubMenu[0], { + open: true, + e: expect.any(Object), + }); }); it('должен открывать subMenu при клике на элемент', () => { From 43bac82ce07496a4e889e058207435da3ea2413e Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Thu, 6 Nov 2025 12:04:36 +0000 Subject: [PATCH 24/32] docs(Navbar): update docs for new onSubMenuToggle signature --- src/components/Navbar/__stand__/Navbar.dev.stand.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index 75ef674..96d7aaa 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -128,7 +128,7 @@ const menu = [ const getItemSubMenuOpen = (item) => openStates[item.label] || false; -const onSubMenuToggle = (item, open, { e }) => { +const onSubMenuToggle = (item, { open, e }) => { setOpenStates((prev) => ({ ...prev, [item.label]: open })); }; @@ -227,8 +227,7 @@ type NavbarPropGetItemSubMenuOpen = (item: ITEM) => boolean | undefined; type NavbarProponOnSubMenuToggle = ( item: ITEM, - open: boolean, - params: { e?: React.MouseEvent }, + params: { open: boolean; e?: React.MouseEvent }, ) => void; ``` From 4af80fe9b7b07347b3752317749ea54f43bd02df Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 7 Nov 2025 07:28:10 +0000 Subject: [PATCH 25/32] fix(Navbar): change getItemSubMenuOpen return type from boolean | undefined to strict boolean --- src/components/Navbar/types.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Navbar/types.ts b/src/components/Navbar/types.ts index 7449864..73206a7 100644 --- a/src/components/Navbar/types.ts +++ b/src/components/Navbar/types.ts @@ -96,9 +96,7 @@ export type NavbarPropSortGroup = ( b: Group, ) => number; -export type NavbarPropGetItemSubMenuOpen = ( - item: ITEM, -) => boolean | undefined; +export type NavbarPropGetItemSubMenuOpen = (item: ITEM) => boolean; export type NavbarPropOnSubMenuToggle = ( item: ITEM, From b9e7ab366d71349d86efe5484eb70275c4a643be Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 7 Nov 2025 07:30:29 +0000 Subject: [PATCH 26/32] docs(Navbar): update getItemSubMenuOpen type documentation --- src/components/Navbar/__stand__/Navbar.dev.stand.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index 96d7aaa..eefb645 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -223,7 +223,7 @@ type NavbarPropGetItemStatus = ( type NavbarPropGetItemSubMenu = (item: ITEM) => ITEM[] | undefined; -type NavbarPropGetItemSubMenuOpen = (item: ITEM) => boolean | undefined; +type NavbarPropGetItemSubMenuOpen = (item: ITEM) => boolean; type NavbarProponOnSubMenuToggle = ( item: ITEM, From f9c9aa6474b1810fe1cf7cebb39cf806c1a7f0c0 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 7 Nov 2025 07:32:28 +0000 Subject: [PATCH 27/32] feat(Navbar): add controlled submenu example --- .../NavbarControlledExample.tsx | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx diff --git a/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx b/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx new file mode 100644 index 0000000..b293399 --- /dev/null +++ b/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx @@ -0,0 +1,103 @@ +import { Example } from '@consta/stand'; +import { Button } from '@consta/uikit/Button'; +import React from 'react'; + +import { Navbar } from '##/components/Navbar'; + +type MenuItem = { + label: string; + subMenu?: MenuItem[]; +}; + +const menu: MenuItem[] = [ + { + label: 'Пункт 1', + subMenu: [ + { label: 'Подпункт 1.1' }, + { label: 'Подпункт 1.2' }, + { label: 'Подпункт 1.3' }, + ], + }, + { + label: 'Пункт 2', + subMenu: [{ label: 'Подпункт 2.1' }, { label: 'Подпункт 2.2' }], + }, + { + label: 'Пункт 3', + subMenu: [{ label: 'Подпункт 3.1' }, { label: 'Подпункт 3.2' }], + }, +]; + +export const NavbarControlledExample = () => { + const [openStates, setOpenStates] = React.useState>({ + 'Пункт 1': true, + 'Пункт 2': false, + 'Пункт 3': false, + }); + + const getItemSubMenuOpen = (item: MenuItem) => + openStates[item.label] || false; + + const onSubMenuToggle = ( + item: MenuItem, + { open }: { open: boolean; e?: React.MouseEvent }, + ) => { + setOpenStates((prev) => ({ ...prev, [item.label]: open })); + }; + + const toggleMenu = (menuLabel: string) => { + setOpenStates((prev) => ({ + ...prev, + [menuLabel]: !prev[menuLabel], + })); + }; + + return ( + +
+
+ +
+
+ {menu.map((item) => ( +
+
+
+ ); +}; From 91c6760747a207bd66bbaa1da328ceb8a92a69b3 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 7 Nov 2025 07:34:02 +0000 Subject: [PATCH 28/32] docs(Navbar): add interactive controlled submenu example --- src/components/Navbar/__stand__/Navbar.dev.stand.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index eefb645..fc3f28c 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -14,6 +14,7 @@ import { import { NavbarSizeExample } from './examples/NavbarSizeExample/NavbarSizeExample'; import { NavbaFormExample } from './examples/NavbaFormExample/NavbaFormExample'; import { NavbarOnClickExample } from './examples/NavbarOnClickExample/NavbarOnClickExample'; +import { NavbarControlledExample } from './examples/NavbarControlledExample/NavbarControlledExample'; import { NavbarRailExample, NavbarRailTooltipExample, @@ -141,6 +142,8 @@ export const NavbarControlledExample = () => ( ); ``` + + ## Свойства Navbar ```tsx From 45c6d7fc8e0c1abe0e77dbbe205d2ed3fab7e4a4 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 7 Nov 2025 08:52:16 +0000 Subject: [PATCH 29/32] feat(Navbar): add CSS styles for controlled submenu example --- .../NavbarControlledExample.css | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.css diff --git a/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.css b/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.css new file mode 100644 index 0000000..1b65ff0 --- /dev/null +++ b/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.css @@ -0,0 +1,19 @@ +.che--NavbarControlledExample { + display: flex; + gap: var(--space-m); + height: 400px; +} + +.che--NavbarControlledExample-Navbar { + overflow: auto; + flex: 1; + padding: var(--space-m); + border: 1px solid var(--color-bg-border); +} + +.che--NavbarControlledExample-Controls { + display: flex; + flex-direction: column; + gap: var(--space-xs); + min-width: 120px; +} From cdd6a97e55c33b30c6d2772dbfe7a8a678ac9c21 Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 7 Nov 2025 08:53:17 +0000 Subject: [PATCH 30/32] refactor(Navbar): update controlled example to use CSS classes --- .../NavbarControlledExample.tsx | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx b/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx index b293399..be8c76a 100644 --- a/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx +++ b/src/components/Navbar/__stand__/examples/NavbarControlledExample/NavbarControlledExample.tsx @@ -1,8 +1,13 @@ +import './NavbarControlledExample.css'; + import { Example } from '@consta/stand'; import { Button } from '@consta/uikit/Button'; import React from 'react'; import { Navbar } from '##/components/Navbar'; +import { cn } from '##/utils/bem'; + +const cnNavbarControlledExample = cn('NavbarControlledExample'); type MenuItem = { label: string; @@ -12,11 +17,7 @@ type MenuItem = { const menu: MenuItem[] = [ { label: 'Пункт 1', - subMenu: [ - { label: 'Подпункт 1.1' }, - { label: 'Подпункт 1.2' }, - { label: 'Подпункт 1.3' }, - ], + subMenu: [{ label: 'Подпункт 1.1' }, { label: 'Подпункт 1.2' }], }, { label: 'Пункт 2', @@ -54,35 +55,15 @@ export const NavbarControlledExample = () => { return ( -
-
+
+
-
+
{menu.map((item) => (
From 37e902b3e4257fc7cba2b5bdcc44084c8d28a76a Mon Sep 17 00:00:00 2001 From: ShavrinAleksei Date: Fri, 7 Nov 2025 08:54:26 +0000 Subject: [PATCH 31/32] docs(Navbar): add interactive tabs for controlled submenu example --- .../Navbar/__stand__/Navbar.dev.stand.mdx | 107 +++++++++++++++--- 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index fc3f28c..83b11d2 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -1,4 +1,4 @@ -import { MdxMenu } from '@consta/stand'; +import { MdxMenu, MdxTabs } from '@consta/stand'; import { NavbarDriverRightExample, @@ -120,28 +120,105 @@ export const NavbarOnClickExample = () => ( Для управления состоянием подменю (открыто/закрыто) используйте связку getItemSubMenuOpen и onSubMenuToggle. Это позволяет контролировать открытие подменю через внешний стейт, включая задание начального состояния. + + ```tsx -const [openStates, setOpenStates] = React.useState({ 'Пункт 1': true }); -const menu = [ - { label: 'Пункт 1', subMenu: [{ label: 'Подпункт 1' }] }, - { label: 'Пункт 2', subMenu: [{ label: 'Подпункт 2' }] }, +type MenuItem = { + label: string; + subMenu?: MenuItem[]; +}; + +const menu: MenuItem[] = [ + { + label: 'Пункт 1', + subMenu: [{ label: 'Подпункт 1.1' }, { label: 'Подпункт 1.2' }], + }, + { + label: 'Пункт 2', + subMenu: [{ label: 'Подпункт 2.1' }, { label: 'Подпункт 2.2' }], + }, + { + label: 'Пункт 3', + subMenu: [{ label: 'Подпункт 3.1' }, { label: 'Подпункт 3.2' }], + }, ]; -const getItemSubMenuOpen = (item) => openStates[item.label] || false; +export const NavbarControlledExample = () => { + const [openStates, setOpenStates] = React.useState>({ + 'Пункт 1': true, + 'Пункт 2': false, + 'Пункт 3': false, + }); + + const getItemSubMenuOpen = (item: MenuItem) => + openStates[item.label] || false; + + const onSubMenuToggle = ( + item: MenuItem, + { open }: { open: boolean; e?: React.MouseEvent }, + ) => { + setOpenStates((prev) => ({ ...prev, [item.label]: open })); + }; -const onSubMenuToggle = (item, { open, e }) => { - setOpenStates((prev) => ({ ...prev, [item.label]: open })); + const toggleMenu = (menuLabel: string) => { + setOpenStates((prev) => ({ + ...prev, + [menuLabel]: !prev[menuLabel], + })); + }; + + return ( +
+
+ +
+
+ {menu.map((item) => ( +
+
+ ); }; +``` -export const NavbarControlledExample = () => ( - -); +```css +.che--NavbarControlledExample { + display: flex; + gap: var(--space-m); + height: 400px; +} + +.che--NavbarControlledExample-Navbar { + flex: 1; + border: 1px solid var(--color-bg-border); + padding: var(--space-m); + overflow: auto; +} + +.che--NavbarControlledExample-Controls { + display: flex; + flex-direction: column; + gap: var(--space-xs); + min-width: 120px; +} ``` +
+ ## Свойства Navbar From 4d6cea9d157286bfaa7fe26772a96a84e20d7b0e Mon Sep 17 00:00:00 2001 From: gizeasy Date: Mon, 10 Nov 2025 10:47:24 +0300 Subject: [PATCH 32/32] docs(Navbar): edit menu --- src/components/Navbar/__stand__/Navbar.dev.stand.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx index 83b11d2..f26552a 100644 --- a/src/components/Navbar/__stand__/Navbar.dev.stand.mdx +++ b/src/components/Navbar/__stand__/Navbar.dev.stand.mdx @@ -32,7 +32,8 @@ import { Navbar } from '@consta/header/Navbar'; - [Размер](#размер) - [Форма](#форма) - [Взаимодействие](#взаимодействие) -- [Cвойства](#свойства-navbar) +- [Управление состоянием подменю](#управление-состоянием-подменю) +- [Свойства](#свойства-navbar) - [NavbarRail](#navbarrail) - [Содержимое](#содержимое-navbarrail) - [Cвойства](#свойства-navbarrail)