feat: Sidebar — пункты-ссылки и раскрывающиеся группы (0.13.0)
ISidebarItem.href открывает пункт как внешнюю ссылку в новой вкладке, ISidebarItem.children делает пункт раскрывающейся группой с подпунктами (ссылки или разделы). Без новых полей меню работает как раньше. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@tnsu/ui-kit",
|
"name": "@tnsu/ui-kit",
|
||||||
"version": "0.0.1",
|
"version": "0.13.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@tnsu/ui-kit",
|
"name": "@tnsu/ui-kit",
|
||||||
"version": "0.0.1",
|
"version": "0.13.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "latest",
|
"@chromatic-com/storybook": "latest",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tnsu/ui-kit",
|
"name": "@tnsu/ui-kit",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"description": "TNSU shared UI components for SPFx web parts",
|
"description": "TNSU shared UI components for SPFx web parts",
|
||||||
"author": "Timur Gorodetskiy",
|
"author": "Timur Gorodetskiy",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|||||||
@@ -97,6 +97,8 @@
|
|||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
// Пункт-ссылка (<a>) выглядит так же, как пункт-кнопка.
|
||||||
|
text-decoration: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color .18s, background .18s, border-color .18s;
|
transition: color .18s, background .18s, border-color .18s;
|
||||||
@@ -153,6 +155,55 @@
|
|||||||
right: 8px;
|
right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Раскрывающаяся группа: подпункты без иконок, с отступом под иконку шапки ----
|
||||||
|
$sub-item-indent: 48px; // 18px отступ пункта + 3px маркер + иконка 16px + зазор
|
||||||
|
|
||||||
|
.group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupChevron {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
color: rgba(255, 255, 255, .45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subItems {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: $sp-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $sp-2;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 18px 8px $sub-item-indent;
|
||||||
|
border: 0;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255, 255, 255, .55);
|
||||||
|
font-family: $font-sans;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color .18s, background .18s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(255, 255, 255, .92);
|
||||||
|
background: rgba(255, 255, 255, .04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subItemActive {
|
||||||
|
color: $white;
|
||||||
|
border-left-color: $gold-500;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Затемнение фона под мобильным drawer ----
|
// ---- Затемнение фона под мобильным drawer ----
|
||||||
.backdrop {
|
.backdrop {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -63,6 +63,39 @@ export const Collapsed: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Пункт с children — раскрывающаяся группа; подпункт с href открывается в новой вкладке.
|
||||||
|
const ITEMS_WITH_GROUP: ISidebarItem[] = [
|
||||||
|
...ITEMS.slice(0, 3),
|
||||||
|
{
|
||||||
|
id: 'catalogs',
|
||||||
|
title: 'Справочники',
|
||||||
|
icon: 'info',
|
||||||
|
children: [
|
||||||
|
{ id: 'companies', title: 'Компании', href: 'https://example.com/companies' },
|
||||||
|
{ id: 'units', title: 'Единицы измерения', href: 'https://example.com/units' },
|
||||||
|
{ id: 'manual', title: 'Инструкция', href: 'https://example.com/manual' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ id: 'help', title: 'Справка', icon: 'chat', href: 'https://example.com/help' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const WithGroupAndLinks: Story = {
|
||||||
|
render: () => {
|
||||||
|
const [active, setActive] = useState('approvals');
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
return (
|
||||||
|
<Sidebar
|
||||||
|
items={ITEMS_WITH_GROUP}
|
||||||
|
activeId={active}
|
||||||
|
onChange={setActive}
|
||||||
|
brand="Портал заявок"
|
||||||
|
collapsed={collapsed}
|
||||||
|
onToggleCollapsed={() => setCollapsed((c) => !c)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Мобильный drawer: сузь окно браузера (или переключи Viewport в панели Storybook)
|
// Мобильный drawer: сузь окно браузера (или переключи Viewport в панели Storybook)
|
||||||
// ниже 768px — сайдбар спрячется, а «☰ Меню» его выдвинет поверх контента.
|
// ниже 768px — сайдбар спрячется, а «☰ Меню» его выдвинет поверх контента.
|
||||||
export const MobileDrawer: Story = {
|
export const MobileDrawer: Story = {
|
||||||
|
|||||||
+31
-31
@@ -1,16 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './Sidebar.module.scss';
|
import styles from './Sidebar.module.scss';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { IconName } from '../Icon/iconNames';
|
import { ISidebarItem } from './sidebarLogic';
|
||||||
|
import { SidebarEntry } from './SidebarEntry';
|
||||||
|
import { SidebarGroup } from './SidebarGroup';
|
||||||
|
|
||||||
export interface ISidebarItem {
|
export type { ISidebarItem, ISidebarSubItem } from './sidebarLogic';
|
||||||
/** Уникальный идентификатор раздела (любая строка). */
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
icon: IconName;
|
|
||||||
/** Счётчик-бейдж справа (например, число заявок в очереди). 0/undefined — не показывать. */
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISidebarProps {
|
export interface ISidebarProps {
|
||||||
items: ISidebarItem[];
|
items: ISidebarItem[];
|
||||||
@@ -33,6 +28,7 @@ export interface ISidebarProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Тёмный боковой рейл на всю высоту: бренд сверху, разделы снизу. Управляемый.
|
* Тёмный боковой рейл на всю высоту: бренд сверху, разделы снизу. Управляемый.
|
||||||
|
* Пункт с href — внешняя ссылка (новая вкладка), с children — раскрывающаяся группа.
|
||||||
* Адаптивный:
|
* Адаптивный:
|
||||||
* • десктоп — рейл, сворачивается в иконки (collapsed);
|
* • десктоп — рейл, сворачивается в иконки (collapsed);
|
||||||
* • < 768px — выезжающий поверх контента drawer с затемнением фона
|
* • < 768px — выезжающий поверх контента drawer с затемнением фона
|
||||||
@@ -58,9 +54,13 @@ export const Sidebar: React.FC<ISidebarProps> = ({
|
|||||||
|
|
||||||
// На мобилке после выбора раздела drawer закрываем; на десктопе onMobileClose
|
// На мобилке после выбора раздела drawer закрываем; на десктопе onMobileClose
|
||||||
// обычно не передан — вызов безвреден.
|
// обычно не передан — вызов безвреден.
|
||||||
|
const closeMobile = (): void => {
|
||||||
|
if (onMobileClose) onMobileClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelect = (id: string): void => {
|
const handleSelect = (id: string): void => {
|
||||||
onChange(id);
|
onChange(id);
|
||||||
if (onMobileClose) onMobileClose();
|
closeMobile();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,28 +84,28 @@ export const Sidebar: React.FC<ISidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.items}>
|
<div className={styles.items}>
|
||||||
{items.map((item) => {
|
{items.map((item) =>
|
||||||
const isActive = item.id === activeId;
|
item.children ? (
|
||||||
const itemClasses = [styles.item, isActive ? styles.itemActive : ''].filter(Boolean).join(' ');
|
<SidebarGroup
|
||||||
const showCount = !!item.count;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
item={item}
|
||||||
className={itemClasses}
|
activeId={activeId}
|
||||||
aria-current={isActive ? 'page' : undefined}
|
collapsed={collapsed}
|
||||||
onClick={() => handleSelect(item.id)}
|
onSelect={handleSelect}
|
||||||
title={collapsed ? item.title : undefined}
|
onLinkOpen={closeMobile}
|
||||||
>
|
onExpandRail={onToggleCollapsed}
|
||||||
<span className={styles.itemIcon}>
|
/>
|
||||||
<Icon name={item.icon} size="sm" />
|
) : (
|
||||||
</span>
|
<SidebarEntry
|
||||||
{!collapsed && <span className={styles.itemTitle}>{item.title}</span>}
|
key={item.id}
|
||||||
{showCount && <span className={styles.badge}>{item.count}</span>}
|
item={item}
|
||||||
</button>
|
isActive={item.id === activeId}
|
||||||
);
|
collapsed={collapsed}
|
||||||
})}
|
onSelect={handleSelect}
|
||||||
|
onLinkOpen={closeMobile}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Sidebar.module.scss';
|
||||||
|
import { ISidebarItem } from './sidebarLogic';
|
||||||
|
import { SidebarItemContent } from './SidebarItemContent';
|
||||||
|
|
||||||
|
interface ISidebarEntryProps {
|
||||||
|
item: ISidebarItem;
|
||||||
|
isActive: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
/** Переход по внешней ссылке — закрыть мобильный drawer. */
|
||||||
|
onLinkOpen: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Обычный пункт: раздел (кнопка, onChange) или внешняя ссылка (новая вкладка). */
|
||||||
|
export const SidebarEntry: React.FC<ISidebarEntryProps> = ({ item, isActive, collapsed, onSelect, onLinkOpen }) => {
|
||||||
|
const classes = [styles.item, isActive ? styles.itemActive : ''].filter(Boolean).join(' ');
|
||||||
|
const title = collapsed ? item.title : undefined;
|
||||||
|
|
||||||
|
if (item.href) {
|
||||||
|
return (
|
||||||
|
<a className={classes} href={item.href} target="_blank" rel="noopener noreferrer" title={title} onClick={onLinkOpen}>
|
||||||
|
<SidebarItemContent item={item} collapsed={collapsed} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classes}
|
||||||
|
aria-current={isActive ? 'page' : undefined}
|
||||||
|
onClick={() => onSelect(item.id)}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<SidebarItemContent item={item} collapsed={collapsed} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Sidebar.module.scss';
|
||||||
|
import { Icon } from '../Icon/Icon';
|
||||||
|
import { hasActiveChild, ISidebarItem } from './sidebarLogic';
|
||||||
|
import { SidebarItemContent } from './SidebarItemContent';
|
||||||
|
import { SidebarSubItem } from './SidebarSubItem';
|
||||||
|
|
||||||
|
interface ISidebarGroupProps {
|
||||||
|
item: ISidebarItem;
|
||||||
|
activeId: string;
|
||||||
|
collapsed: boolean;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
onLinkOpen: () => void;
|
||||||
|
/** Свёрнутый рейл: подпунктам некуда раскрыться — сначала разворачиваем рейл. */
|
||||||
|
onExpandRail: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Раскрывающаяся группа. Сама разделом не является: клик раскрывает/сворачивает подпункты.
|
||||||
|
* Группа с активным подпунктом всегда раскрыта — иначе текущий раздел не виден в меню.
|
||||||
|
*/
|
||||||
|
export const SidebarGroup: React.FC<ISidebarGroupProps> = ({
|
||||||
|
item,
|
||||||
|
activeId,
|
||||||
|
collapsed,
|
||||||
|
onSelect,
|
||||||
|
onLinkOpen,
|
||||||
|
onExpandRail,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = React.useState<boolean>(false);
|
||||||
|
const containsActive = hasActiveChild(item, activeId);
|
||||||
|
const isExpanded = isOpen || containsActive;
|
||||||
|
const listId = `sidebar-group-${item.id}`;
|
||||||
|
|
||||||
|
const toggle = (): void => {
|
||||||
|
if (collapsed) {
|
||||||
|
onExpandRail();
|
||||||
|
setIsOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsOpen((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerClasses = [styles.item, containsActive ? styles.itemActive : ''].filter(Boolean).join(' ');
|
||||||
|
const chevron = (
|
||||||
|
<span className={styles.groupChevron}>
|
||||||
|
<Icon name={isExpanded ? 'chevron-down' : 'chevron-right'} size="sm" />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.group}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={headerClasses}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
aria-controls={listId}
|
||||||
|
onClick={toggle}
|
||||||
|
title={collapsed ? item.title : undefined}
|
||||||
|
>
|
||||||
|
<SidebarItemContent item={item} collapsed={collapsed} trailing={chevron} />
|
||||||
|
</button>
|
||||||
|
{!collapsed && isExpanded && (
|
||||||
|
<div id={listId} className={styles.subItems}>
|
||||||
|
{(item.children || []).map((child) => (
|
||||||
|
<SidebarSubItem
|
||||||
|
key={child.id}
|
||||||
|
item={child}
|
||||||
|
isActive={child.id === activeId}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onLinkOpen={onLinkOpen}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Sidebar.module.scss';
|
||||||
|
import { Icon } from '../Icon/Icon';
|
||||||
|
import { ISidebarItem } from './sidebarLogic';
|
||||||
|
|
||||||
|
interface ISidebarItemContentProps {
|
||||||
|
item: ISidebarItem;
|
||||||
|
collapsed: boolean;
|
||||||
|
/** Справа после названия (стрелка группы). */
|
||||||
|
trailing?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Иконка, название и бейдж пункта — одинаковые у раздела, ссылки и шапки группы. */
|
||||||
|
export const SidebarItemContent: React.FC<ISidebarItemContentProps> = ({ item, collapsed, trailing }) => (
|
||||||
|
<>
|
||||||
|
<span className={styles.itemIcon}>
|
||||||
|
<Icon name={item.icon} size="sm" />
|
||||||
|
</span>
|
||||||
|
{!collapsed && <span className={styles.itemTitle}>{item.title}</span>}
|
||||||
|
{!!item.count && <span className={styles.badge}>{item.count}</span>}
|
||||||
|
{!collapsed && trailing}
|
||||||
|
</>
|
||||||
|
);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Sidebar.module.scss';
|
||||||
|
import { ISidebarSubItem } from './sidebarLogic';
|
||||||
|
|
||||||
|
interface ISidebarSubItemProps {
|
||||||
|
item: ISidebarSubItem;
|
||||||
|
isActive: boolean;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
onLinkOpen: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Подпункт группы: внешняя ссылка (новая вкладка) или раздел (onChange). */
|
||||||
|
export const SidebarSubItem: React.FC<ISidebarSubItemProps> = ({ item, isActive, onSelect, onLinkOpen }) => {
|
||||||
|
const classes = [styles.subItem, isActive ? styles.subItemActive : ''].filter(Boolean).join(' ');
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
<span className={styles.itemTitle}>{item.title}</span>
|
||||||
|
{!!item.count && <span className={styles.badge}>{item.count}</span>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (item.href) {
|
||||||
|
return (
|
||||||
|
<a className={classes} href={item.href} target="_blank" rel="noopener noreferrer" onClick={onLinkOpen}>
|
||||||
|
{content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<button type="button" className={classes} aria-current={isActive ? 'page' : undefined} onClick={() => onSelect(item.id)}>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { hasActiveChild, isItemHighlighted, ISidebarItem } from './sidebarLogic';
|
||||||
|
|
||||||
|
const PLAIN: ISidebarItem = { id: 'objects', title: 'Объекты', icon: 'folder' };
|
||||||
|
const GROUP: ISidebarItem = {
|
||||||
|
id: 'catalogs',
|
||||||
|
title: 'Справочники',
|
||||||
|
icon: 'info',
|
||||||
|
children: [
|
||||||
|
{ id: 'companies', title: 'Компании', href: 'https://example.com/companies' },
|
||||||
|
{ id: 'settings', title: 'Настройки' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('hasActiveChild — раскрытие группы', () => {
|
||||||
|
it('активен подпункт — группа раскрыта', () => {
|
||||||
|
expect(hasActiveChild(GROUP, 'settings')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('активен другой раздел или пункт без вложенности — нет', () => {
|
||||||
|
expect(hasActiveChild(GROUP, 'objects')).toBe(false);
|
||||||
|
expect(hasActiveChild(PLAIN, 'objects')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isItemHighlighted — подсветка пункта', () => {
|
||||||
|
it('обычный пункт — по своему id', () => {
|
||||||
|
expect(isItemHighlighted(PLAIN, 'objects')).toBe(true);
|
||||||
|
expect(isItemHighlighted(PLAIN, 'catalogs')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('группа — когда активен её подпункт', () => {
|
||||||
|
expect(isItemHighlighted(GROUP, 'settings')).toBe(true);
|
||||||
|
expect(isItemHighlighted(GROUP, 'objects')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { IconName } from '../Icon/iconNames';
|
||||||
|
|
||||||
|
/** Подпункт раскрывающейся группы. Без иконки — отступ показывает вложенность. */
|
||||||
|
export interface ISidebarSubItem {
|
||||||
|
/** Уникальный идентификатор (среди всех пунктов меню). */
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
/** Внешняя ссылка: открывается в новой вкладке, onChange не вызывается. */
|
||||||
|
href?: string;
|
||||||
|
/** Счётчик-бейдж справа. 0/undefined — не показывать. */
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISidebarItem {
|
||||||
|
/** Уникальный идентификатор раздела (любая строка). */
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: IconName;
|
||||||
|
/** Счётчик-бейдж справа (например, число заявок в очереди). 0/undefined — не показывать. */
|
||||||
|
count?: number;
|
||||||
|
/** Внешняя ссылка: пункт открывается в новой вкладке, onChange не вызывается. */
|
||||||
|
href?: string;
|
||||||
|
/**
|
||||||
|
* Вложенные пункты: пункт становится раскрывающейся группой (клик — раскрыть/свернуть),
|
||||||
|
* сам он разделом не является и onChange не вызывает.
|
||||||
|
*/
|
||||||
|
children?: ISidebarSubItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Активный раздел — один из подпунктов группы: группу показываем раскрытой и подсвеченной. */
|
||||||
|
export function hasActiveChild(item: ISidebarItem, activeId: string): boolean {
|
||||||
|
return (item.children || []).some((child) => child.id === activeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Подсветка пункта: сам раздел активен или (для группы) активен подпункт. */
|
||||||
|
export function isItemHighlighted(item: ISidebarItem, activeId: string): boolean {
|
||||||
|
return item.id === activeId || hasActiveChild(item, activeId);
|
||||||
|
}
|
||||||
+1
-1
@@ -73,7 +73,7 @@ export type { ITab, ITabsProps } from './Tabs/Tabs';
|
|||||||
export { Stepper, highestReachableIndex } from './Stepper/Stepper';
|
export { Stepper, highestReachableIndex } from './Stepper/Stepper';
|
||||||
export type { IStepperStep, IStepperProps } from './Stepper/Stepper';
|
export type { IStepperStep, IStepperProps } from './Stepper/Stepper';
|
||||||
export { Sidebar } from './Sidebar/Sidebar';
|
export { Sidebar } from './Sidebar/Sidebar';
|
||||||
export type { ISidebarItem, ISidebarProps } from './Sidebar/Sidebar';
|
export type { ISidebarItem, ISidebarSubItem, ISidebarProps } from './Sidebar/Sidebar';
|
||||||
export { SidebarToggle } from './Sidebar/SidebarToggle';
|
export { SidebarToggle } from './Sidebar/SidebarToggle';
|
||||||
export type { ISidebarToggleProps } from './Sidebar/SidebarToggle';
|
export type { ISidebarToggleProps } from './Sidebar/SidebarToggle';
|
||||||
export { Link } from './Link/Link';
|
export { Link } from './Link/Link';
|
||||||
|
|||||||
Reference in New Issue
Block a user